Spaces:
Paused
Paused
| import gradio as gr | |
| import random | |
| import time | |
| from typing import Dict, List, Tuple | |
| # Game state storage | |
| game_rooms = {} | |
| class Player: | |
| def __init__(self, name: str): | |
| self.name = name | |
| self.hp = 100 | |
| self.max_hp = 100 | |
| self.attack = 15 | |
| self.potions = 3 | |
| self.gold = 0 | |
| self.x = 0 | |
| self.y = 0 | |
| self.level = 1 | |
| self.exp = 0 | |
| class Room: | |
| def __init__(self, x: int, y: int): | |
| self.x = x | |
| self.y = y | |
| self.monster = None | |
| self.treasure = None | |
| self.explored = False | |
| self.generate_content() | |
| def generate_content(self): | |
| rand = random.randint(1,100) | |
| if rand <= 50: | |
| monsters = [ | |
| ("Goblin", 30, 8, 10, 15), | |
| ("Orc", 50, 12, 20, 25), | |
| ("Skeleton", 40, 10, 15, 20), | |
| ("Dark Mage", 35, 15, 25, 30), | |
| ("Dragon Whelp", 60, 18, 30, 40) | |
| ] | |
| name, hp, atk, gold, exp = random.choice(monsters) | |
| self.monster = {"name": name, "hp": hp, "max_hp": hp, "attack": atk, "gold": gold, "exp": exp} | |
| elif rand <= 75: | |
| self.treasure = random.randint(10, 40) | |
| elif rand <= 90: | |
| self.treasure = "potion" | |
| elif rand <= 95: | |
| self.treasure = "weapon" | |
| class GameRoom: | |
| def __init__(self, room_id: str): | |
| self.room_id = room_id | |
| self.players: Dict[str, Player] = {} | |
| self.grid: Dict[Tuple[int, int], Room] = {} | |
| self.current_player = "" | |
| self.turn_count = 0 | |
| self.battle_active = False | |
| self.current_monster = None | |
| self.log = ["🏰 Game dimulai! Gabung sekarang!"] | |
| # Generate dungeon grid (7x7 for more exploration) | |
| for x in range(7): | |
| for y in range(7): | |
| self.grid[(x, y)] = Room(x, y) | |
| # Place exit | |
| exit_x, exit_y = 6, 6 | |
| self.grid[(exit_x, exit_y)].treasure = "exit" | |
| # Place special rooms | |
| self.grid[(3, 3)].treasure = "boss" | |
| self.grid[(0, 6)].treasure = "artifact" | |
| def add_player(self, player_name: str) -> str: | |
| if player_name in self.players: | |
| return f"{player_name} sudah ada dalam room!" | |
| if len(self.players) >= 4: | |
| return "Room penuh! Maksimal 4 pemain." | |
| self.players[player_name] = Player(player_name) | |
| self.log.append(f"🎮 {player_name} bergabung dalam petualangan!") | |
| if not self.current_player: | |
| self.current_player = player_name | |
| self.log.append(f"👑 {player_name} memulai petualangan!") | |
| return f"🎉 {player_name} berhasil bergabung!" | |
| def get_player_status(self, player_name: str) -> str: | |
| if player_name not in self.players: | |
| return "Pemain tidak ditemukan!" | |
| p = self.players[player_name] | |
| # Create HP bar | |
| hp_percent = (p.hp / p.max_hp) * 100 | |
| hp_bar = self.create_progress_bar(hp_percent, "❤️") | |
| exp_bar = self.create_progress_bar(p.exp, "🌟") | |
| return f""" | |
| <div style="background: linear-gradient(135deg, #2c3e50, #4a69bd); padding: 15px; border-radius: 10px; color: white; border: 2px solid #f39c12;"> | |
| <h3 style="margin: 0 0 10px 0; color: #f39c12;">🏹 {p.name}</h3> | |
| {hp_bar} | |
| <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 10px;"> | |
| <div>⚔️ <strong>Attack:</strong> {p.attack}</div> | |
| <div>🧪 <strong>Potions:</strong> {p.potions}</div> | |
| <div>💰 <strong>Gold:</strong> {p.gold}</div> | |
| <div>🎯 <strong>Level:</strong> {p.level}</div> | |
| </div> | |
| {exp_bar} | |
| <div style="margin-top: 8px;">📍 <strong>Position:</strong> ({p.x}, {p.y})</div> | |
| </div> | |
| """ | |
| def create_progress_bar(self, percent, label): | |
| width = min(percent, 100) | |
| color = "#2ecc71" if percent > 70 else "#f39c12" if percent > 30 else "#e74c3c" | |
| return f""" | |
| <div style="margin: 5px 0;"> | |
| <div style="display: flex; justify-content: between; font-size: 12px;"> | |
| <span>{label}</span> | |
| <span>{percent:.1f}%</span> | |
| </div> | |
| <div style="background: #34495e; height: 8px; border-radius: 4px; overflow: hidden;"> | |
| <div style="background: {color}; height: 100%; width: {width}%; border-radius: 4px;"></div> | |
| </div> | |
| </div> | |
| """ | |
| def move_player(self, player_name: str, direction: str) -> Tuple[bool, str]: | |
| if player_name != self.current_player: | |
| return False, "❌ Bukan giliranmu!" | |
| if player_name not in self.players: | |
| return False, "❌ Pemain tidak ada!" | |
| if self.battle_active: | |
| return False, "❌ Sedang dalam pertarungan!" | |
| p = self.players[player_name] | |
| new_x, new_y = p.x, p.y | |
| if direction == "utara" and p.y > 0: | |
| new_y -= 1 | |
| elif direction == "selatan" and p.y < 6: | |
| new_y += 1 | |
| elif direction == "barat" and p.x > 0: | |
| new_x -= 1 | |
| elif direction == "timur" and p.x < 6: | |
| new_x += 1 | |
| else: | |
| return False, "❌ Tidak bisa bergerak ke arah itu!" | |
| p.x, p.y = new_x, new_y | |
| room = self.grid[(new_x, new_y)] | |
| if not room.explored: | |
| room.explored = True | |
| self.log.append(f"🔍 {p.name} menemukan ruangan ({new_x}, {new_y})") | |
| if room.monster: | |
| self.battle_active = True | |
| self.current_monster = room.monster | |
| return True, self.create_battle_alert(f"⚔️ {room.monster['name']} muncul!", f"❤️ HP: {room.monster['hp']} | ⚔️ Attack: {room.monster['attack']}") | |
| elif room.treasure: | |
| if room.treasure == "potion": | |
| p.potions += 1 | |
| self.log.append(f"🧪 {p.name} menemukan ramuan penyembuh!") | |
| return True, self.create_success_alert("🧪 Kamu menemukan ramuan penyembuh!") | |
| elif room.treasure == "weapon": | |
| p.attack += 5 | |
| self.log.append(f"⚔️ {p.name} menemukan senjata legendaris! Attack +5") | |
| return True, self.create_success_alert("⚔️ Kamu menemukan senjata legendaris! Attack +5") | |
| elif room.treasure == "exit": | |
| self.log.append(f"🎉 {p.name} menemukan pintu keluar! Kemenangan!") | |
| return True, self.create_victory_alert("🎉 Selamat! Kamu menemukan pintu keluar!") | |
| elif room.treasure == "boss": | |
| boss = {"name": "Dragon Lord", "hp": 100, "max_hp": 100, "attack": 25, "gold": 100, "exp": 100} | |
| self.battle_active = True | |
| self.current_monster = boss | |
| return True, self.create_battle_alert("🐉 BOSS BESAR! Dragon Lord muncul!", f"❤️ HP: {boss['hp']} | ⚔️ Attack: {boss['attack']}") | |
| elif room.treasure == "artifact": | |
| p.max_hp += 25 | |
| p.hp = p.max_hp | |
| self.log.append(f"💎 {p.name} menemukan artifact! Max HP +25") | |
| return True, self.create_success_alert("💎 Kamu menemukan artifact kuno! Max HP +25") | |
| else: | |
| p.gold += room.treasure | |
| self.log.append(f"💰 {p.name} menemukan {room.treasure} emas!") | |
| return True, self.create_success_alert(f"💰 Kamu menemukan {room.treasure} emas!") | |
| return True, "📍 Bergerak ke ruangan yang sudah dijelajahi." | |
| def create_battle_alert(self, title, content): | |
| return f""" | |
| <div style="background: linear-gradient(135deg, #e74c3c, #c0392b); padding: 15px; border-radius: 10px; color: white; border: 2px solid #f1c40f; text-align: center;"> | |
| <h3 style="margin: 0 0 10px 0;">{title}</h3> | |
| <p style="margin: 0; font-size: 14px;">{content}</p> | |
| </div> | |
| """ | |
| def create_success_alert(self, message): | |
| return f""" | |
| <div style="background: linear-gradient(135deg, #27ae60, #2ecc71); padding: 15px; border-radius: 10px; color: white; border: 2px solid #f1c40f; text-align: center;"> | |
| <h3 style="margin: 0;">{message}</h3> | |
| </div> | |
| """ | |
| def create_victory_alert(self, message): | |
| return f""" | |
| <div style="background: linear-gradient(135deg, #f39c12, #e67e22); padding: 20px; border-radius: 10px; color: white; border: 3px solid #f1c40f; text-align: center;"> | |
| <h2 style="margin: 0;">🎉 {message} 🎉</h2> | |
| </div> | |
| """ | |
| def battle_action(self, player_name: str, action: str) -> Tuple[bool, str]: | |
| if player_name != self.current_player: | |
| return False, "❌ Bukan giliranmu!" | |
| if not self.battle_active: | |
| return False, "❌ Tidak ada pertarungan!" | |
| p = self.players[player_name] | |
| m = self.current_monster | |
| if action == "attack": | |
| dmg = random.randint(p.attack-3, p.attack+5) | |
| m["hp"] -= dmg | |
| msg = f"<div style='text-align: center;'><h3>⚔️ {p.name} menyerang {m['name']}!</h3><p>💥 Damage: <strong>{dmg}</strong></p>" | |
| if m["hp"] <= 0: | |
| p.gold += m["gold"] | |
| p.exp += m["exp"] | |
| msg += f"<p>💀 <strong>{m['name']} dikalahkan!</strong></p><p>💰 +{m['gold']} emas | 🌟 +{m['exp']} EXP</p>" | |
| # Level up check | |
| if p.exp >= 100: | |
| p.level += 1 | |
| p.exp = 0 | |
| p.max_hp += 10 | |
| p.hp = p.max_hp | |
| p.attack += 3 | |
| msg += f"<p>🎉 <strong>LEVEL UP!</strong> Sekarang Level {p.level}!</p>" | |
| self.battle_active = False | |
| self.current_monster = None | |
| self.log.append(f"🎯 {p.name} mengalahkan {m['name']}!") | |
| else: | |
| mdmg = random.randint(m["attack"]-2, m["attack"]+3) | |
| p.hp -= mdmg | |
| msg += f"<p>🩸 <strong>{m['name']}</strong> menyerang balik!</p><p>💔 Damage: <strong>{mdmg}</strong></p>" | |
| self.log.append(f"⚔️ Pertarungan {p.name} vs {m['name']}!") | |
| if p.hp <= 0: | |
| self.log.append(f"💀 {p.name} tewas dalam pertarungan!") | |
| return True, self.create_battle_alert("💀 Kamu tewas dalam pertempuran!", "Game over!") | |
| msg += "</div>" | |
| elif action == "potion": | |
| if p.potions > 0: | |
| heal = random.randint(25, 40) | |
| p.hp = min(p.hp + heal, p.max_hp) | |
| p.potions -= 1 | |
| msg = f"<div style='text-align: center;'><h3>🧪 {p.name} menggunakan ramuan!</h3><p>💚 +{heal} HP</p>" | |
| mdmg = random.randint(m["attack"]-2, m["attack"]+3) | |
| p.hp -= mdmg | |
| msg += f"<p>🩸 <strong>{m['name']}</strong> menyerang!</p><p>💔 Damage: <strong>{mdmg}</strong></p>" | |
| if p.hp <= 0: | |
| self.log.append(f"💀 {p.name} tewas setelah menggunakan ramuan!") | |
| return True, self.create_battle_alert("💀 Kamu tewas dalam pertempuran!", "Game over!") | |
| msg += "</div>" | |
| else: | |
| return False, "❌ Tidak punya ramuan!" | |
| elif action == "flee": | |
| flee_chance = random.randint(1, 100) | |
| if flee_chance > 50: | |
| self.battle_active = False | |
| self.current_monster = None | |
| msg = self.create_success_alert("🏃♂️ Kamu berhasil melarikan diri!") | |
| self.log.append(f"🏃♂️ {p.name} melarikan diri dari pertarungan!") | |
| else: | |
| mdmg = random.randint(m["attack"]-2, m["attack"]+3) | |
| p.hp -= mdmg | |
| msg = f"<div style='text-align: center; background: #e74c3c; padding: 15px; border-radius: 10px; color: white;'><h3>❌ Gagal melarikan diri!</h3><p>🩸 {m['name']} menyerang! Damage: {mdmg}</p></div>" | |
| if p.hp <= 0: | |
| self.log.append(f"💀 {p.name} tewas saat mencoba melarikan diri!") | |
| return True, self.create_battle_alert("💀 Kamu tewas dalam pertempuran!", "Game over!") | |
| return True, msg | |
| def end_turn(self, player_name: str) -> Tuple[bool, str]: | |
| if player_name != self.current_player: | |
| return False, "❌ Bukan giliranmu!" | |
| player_names = list(self.players.keys()) | |
| if len(player_names) <= 1: | |
| return False, "❌ Butuh minimal 2 pemain!" | |
| current_idx = player_names.index(player_name) | |
| next_idx = (current_idx + 1) % len(player_names) | |
| self.current_player = player_names[next_idx] | |
| self.turn_count += 1 | |
| # Heal 5 HP at turn start | |
| p = self.players[self.current_player] | |
| old_hp = p.hp | |
| p.hp = min(p.hp + 5, p.max_hp) | |
| heal_amount = p.hp - old_hp | |
| self.log.append(f"🔄 Giliran {self.current_player} (+{heal_amount} HP)") | |
| return True, f""" | |
| <div style="background: linear-gradient(135deg, #9b59b6, #8e44ad); padding: 15px; border-radius: 10px; color: white; text-align: center;"> | |
| <h3>✅ Giliran {self.current_player}!</h3> | |
| <p>+{heal_amount} HP regenerasi</p> | |
| </div> | |
| """ | |
| def create_dungeon_map_html(self) -> str: | |
| html = """ | |
| <div style="background: #2c3e50; padding: 15px; border-radius: 10px; border: 2px solid #f39c12;"> | |
| <h3 style="color: white; text-align: center; margin-bottom: 15px;">🗺️ Peta Dungeon</h3> | |
| <div style="display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; background: #34495e; padding: 10px; border-radius: 5px;"> | |
| """ | |
| for y in range(7): | |
| for x in range(7): | |
| room = self.grid[(x, y)] | |
| players_here = [p for p in self.players.values() if p.x == x and p.y == y] | |
| room_class = "room " | |
| room_text = "?" | |
| room_title = f"Position: ({x}, {y})" | |
| if players_here: | |
| room_class += "player-room" | |
| room_text = "👤" | |
| room_title += f"\nPlayers: {', '.join([p.name for p in players_here])}" | |
| elif room.treasure == "exit": | |
| room_class += "exit-room" | |
| room_text = "🚪" | |
| room_title += "\nEXIT" | |
| elif room.treasure == "boss": | |
| room_class += "boss-room" | |
| room_text = "🐉" | |
| room_title += "\nBOSS ROOM" | |
| elif room.treasure == "artifact": | |
| room_class += "artifact-room" | |
| room_text = "💎" | |
| room_title += "\nARTIFACT" | |
| elif room.monster and not room.explored: | |
| room_class += "monster-room" | |
| room_text = "👹" | |
| room_title += f"\nMonster: {room.monster['name']}" | |
| elif room.treasure and not room.explored and room.treasure not in ["exit", "boss", "artifact"]: | |
| if room.treasure == "potion": | |
| room_class += "potion-room" | |
| room_text = "🧪" | |
| room_title += "\nPotion" | |
| elif room.treasure == "weapon": | |
| room_class += "weapon-room" | |
| room_text = "⚔️" | |
| room_title += "\nWeapon" | |
| else: | |
| room_class += "treasure-room" | |
| room_text = "💰" | |
| room_title += f"\nGold: {room.treasure}" | |
| elif room.explored: | |
| room_class += "explored-room" | |
| room_text = "✓" | |
| room_title += "\nExplored" | |
| else: | |
| room_class += "unexplored-room" | |
| room_text = "?" | |
| room_title += "\nUnexplored" | |
| html += f""" | |
| <div class="{room_class}" title="{room_title}" style=" | |
| width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; | |
| border-radius: 5px; font-size: 16px; cursor: pointer; border: 1px solid #7f8c8d; | |
| ">{room_text}</div> | |
| """ | |
| html += """ | |
| </div> | |
| <div style="margin-top: 15px; display: grid; grid-template-columns: repeat(3, 1fr); gap: 5px; color: white; font-size: 12px;"> | |
| <div>👤 = Pemain</div> | |
| <div>🚪 = Exit</div> | |
| <div>🐉 = Boss</div> | |
| <div>👹 = Monster</div> | |
| <div>💰 = Treasure</div> | |
| <div>🧪 = Potion</div> | |
| </div> | |
| </div> | |
| <style> | |
| .player-room { background: #f39c12; color: black; } | |
| .exit-room { background: #27ae60; color: white; } | |
| .boss-room { background: #e74c3c; color: white; animation: pulse 2s infinite; } | |
| .artifact-room { background: #9b59b6; color: white; } | |
| .monster-room { background: #e67e22; color: white; } | |
| .treasure-room { background: #f1c40f; color: black; } | |
| .potion-room { background: #3498db; color: white; } | |
| .weapon-room { background: #95a5a6; color: white; } | |
| .explored-room { background: #34495e; color: #bdc3c7; } | |
| .unexplored-room { background: #2c3e50; color: #7f8c8d; } | |
| @keyframes pulse { | |
| 0% { transform: scale(1); } | |
| 50% { transform: scale(1.1); } | |
| 100% { transform: scale(1); } | |
| } | |
| </style> | |
| """ | |
| return html | |
| def get_all_players_html(self) -> str: | |
| if not self.players: | |
| return "<div style='color: #7f8c8d; text-align: center;'>Belum ada pemain</div>" | |
| html = """ | |
| <div style="background: linear-gradient(135deg, #34495e, #2c3e50); padding: 15px; border-radius: 10px; border: 2px solid #f39c12;"> | |
| <h3 style="color: white; margin-top: 0; text-align: center;">👥 Tim Petualang</h3> | |
| """ | |
| for name, p in self.players.items(): | |
| current = "👑" if name == self.current_player else "👤" | |
| hp_percent = (p.hp / p.max_hp) * 100 | |
| hp_color = "#2ecc71" if hp_percent > 70 else "#f39c12" if hp_percent > 30 else "#e74c3c" | |
| html += f""" | |
| <div style="background: #2c3e50; padding: 10px; margin: 8px 0; border-radius: 8px; border-left: 4px solid {hp_color};"> | |
| <div style="display: flex; justify-content: between; align-items: center; color: white;"> | |
| <span style="font-weight: bold; font-size: 16px;">{current} {name}</span> | |
| <span style="font-size: 12px;">Lvl {p.level}</span> | |
| </div> | |
| <div style="color: #bdc3c7; font-size: 12px; margin-top: 5px;"> | |
| ❤️ {p.hp}/{p.max_hp} | ⚔️ {p.attack} | 💰 {p.gold} | |
| </div> | |
| </div> | |
| """ | |
| html += "</div>" | |
| return html | |
| def get_logs_html(self) -> str: | |
| logs = self.log[-8:] if self.log else ["Tidak ada aktivitas"] | |
| html = """ | |
| <div style="background: #1a1a1a; padding: 15px; border-radius: 10px; border: 2px solid #f39c12; height: 200px; overflow-y: auto;"> | |
| <h3 style="color: white; margin-top: 0; text-align: center;">📜 Catatan Petualangan</h3> | |
| """ | |
| for log in logs: | |
| html += f""" | |
| <div style="color: #bdc3c7; padding: 5px 0; border-bottom: 1px solid #2c3e50; font-size: 14px;"> | |
| {log} | |
| </div> | |
| """ | |
| html += "</div>" | |
| return html | |
| # Gradio interface dengan tema yang lebih keren | |
| def create_interface(): | |
| with gr.Blocks( | |
| title="Dungeon Crawler RPG", | |
| theme=gr.themes.Soft(primary_hue="red", secondary_hue="slate"), | |
| css=""" | |
| .dungeon-title { | |
| text-align: center; | |
| background: linear-gradient(45deg, #8B4513, #D2691E); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| font-weight: bold; | |
| font-size: 2.5em; | |
| text-shadow: 2px 2px 4px rgba(0,0,0,0.3); | |
| } | |
| .game-container { | |
| border: 2px solid #8B4513; | |
| border-radius: 10px; | |
| padding: 15px; | |
| background: #f5f5dc; | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.1); | |
| } | |
| """ | |
| ) as demo: | |
| gr.Markdown(""" | |
| <div class='dungeon-title'>🏰 Dungeon Crawler RPG</div> | |
| <div style='text-align: center; color: #666; margin-bottom: 20px; font-size: 16px;'> | |
| Petualangan Multiplayer - Jelajahi dungeon, kalahkan monster, dan temukan harta karun! | |
| </div> | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| with gr.Group(): | |
| gr.Markdown("### 🎮 Masuk Game") | |
| room_id = gr.Textbox( | |
| label="🏠 Room ID", | |
| placeholder="Masukkan Room ID (contoh: dragon_lair)", | |
| info="Buat room baru atau gabung yang sudah ada" | |
| ) | |
| player_name = gr.Textbox( | |
| label="👤 Nama Petualang", | |
| placeholder="Nama pahlawan kamu", | |
| info="Gunakan nama yang keren!" | |
| ) | |
| join_btn = gr.Button("🚪 Masuk Dungeon", variant="primary", size="lg") | |
| with gr.Column(scale=2): | |
| with gr.Group(): | |
| gr.Markdown("### 📊 Status Petualang") | |
| status_box = gr.HTML(label="Status", value="<div style='text-align: center; color: #666;'>Bergabunglah untuk memulai...</div>") | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| with gr.Group(): | |
| gr.Markdown("### 🗺️ Peta Dungeon") | |
| map_box = gr.HTML(label="Peta") | |
| with gr.Column(scale=1): | |
| with gr.Group(): | |
| gr.Markdown("### 👥 Tim Petualang") | |
| players_box = gr.HTML(label="Pemain") | |
| with gr.Group(): | |
| gr.Markdown("### 🔄 Kontrol Game") | |
| turn_info = gr.HTML(label="Giliran", value="<div style='text-align: center; color: #666;'>Menunggu pemain...</div>") | |
| with gr.Row(): | |
| refresh_btn = gr.Button("🔄 Refresh", variant="secondary") | |
| end_turn_btn = gr.Button("⏭️ Akhiri Giliran", variant="primary") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| with gr.Group(): | |
| gr.Markdown("### 🎯 Aksi Petualangan") | |
| with gr.Row(): | |
| move_choice = gr.Radio( | |
| choices=["⬆️ Utara", "⬇️ Selatan", "⬅️ Barat", "➡️ Timur"], | |
| label="🚶 Arah Gerakan", | |
| value="⬆️ Utara" | |
| ) | |
| move_btn = gr.Button("🎯 Jelajahi", variant="primary", size="lg") | |
| with gr.Column(scale=1): | |
| with gr.Group(): | |
| gr.Markdown("### ⚔️ Aksi Pertempuran") | |
| battle_choice = gr.Radio( | |
| choices=["⚔️ Serang", "🧪 Gunakan Ramuan", "🏃♂️ Kabur"], | |
| label="Pilih Strategi", | |
| value="⚔️ Serang" | |
| ) | |
| battle_btn = gr.Button("⚡ Eksekusi", variant="primary", size="lg") | |
| with gr.Row(): | |
| with gr.Group(): | |
| gr.Markdown("### 📜 Catatan Petualangan") | |
| log_box = gr.HTML(label="Activity Log") | |
| # Event handlers | |
| def join_room(room_id_str, name_str): | |
| if not room_id_str or not name_str: | |
| return ( | |
| "❌ Room ID dan nama harus diisi!", | |
| "<div style='text-align: center; color: #666;'>Bergabunglah untuk memulai...</div>", | |
| "<div style='text-align: center; color: #666;'>Peta akan muncul di sini...</div>", | |
| "<div style='color: #7f8c8d; text-align: center;'>Belum ada pemain</div>", | |
| "<div style='text-align: center; color: #666;'>Menunggu pemain...</div>", | |
| "<div style='text-align: center; color: #666;'>Log akan muncul di sini...</div>" | |
| ) | |
| if room_id_str not in game_rooms: | |
| game_rooms[room_id_str] = GameRoom(room_id_str) | |
| room = game_rooms[room_id_str] | |
| result = room.add_player(name_str) | |
| status = room.get_player_status(name_str) | |
| turn = f"<div style='background: linear-gradient(135deg, #f39c12, #e67e22); padding: 10px; border-radius: 8px; color: white; text-align: center;'><strong>👑 Giliran:</strong> {room.current_player}</div>" | |
| map_html = room.create_dungeon_map_html() | |
| players = room.get_all_players_html() | |
| logs = room.get_logs_html() | |
| return result, status, map_html, players, turn, logs | |
| def handle_move(room_id_str, name_str, direction): | |
| direction = direction.replace("⬆️ ", "").replace("⬇️ ", "").replace("⬅️ ", "").replace("➡️ ", "").lower() | |
| if not room_id_str or not name_str: | |
| return "❌ Room ID dan nama harus diisi!", "", "", "" | |
| if room_id_str not in game_rooms: | |
| return "❌ Room tidak ditemukan!", "", "", "" | |
| room = game_rooms[room_id_str] | |
| success, msg = room.move_player(name_str, direction) | |
| status = room.get_player_status(name_str) | |
| map_html = room.create_dungeon_map_html() | |
| logs = room.get_logs_html() | |
| return msg, status, map_html, logs | |
| def handle_battle(room_id_str, name_str, action): | |
| action_map = { | |
| "⚔️ Serang": "attack", | |
| "🧪 Gunakan Ramuan": "potion", | |
| "🏃♂️ Kabur": "flee" | |
| } | |
| action = action_map.get(action, "attack") | |
| if not room_id_str or not name_str: | |
| return "❌ Room ID dan nama harus diisi!", "", "", "" | |
| if room_id_str not in game_rooms: | |
| return "❌ Room tidak ditemukan!", "", "", "" | |
| room = game_rooms[room_id_str] | |
| success, msg = room.battle_action(name_str, action) | |
| status = room.get_player_status(name_str) | |
| map_html = room.create_dungeon_map_html() | |
| logs = room.get_logs_html() | |
| return msg, status, map_html, logs | |
| def handle_end_turn(room_id_str, name_str): | |
| if not room_id_str or not name_str: | |
| return "❌ Room ID dan nama harus diisi!", "", "", "", "" | |
| if room_id_str not in game_rooms: | |
| return "❌ Room tidak ditemukan!", "", "", "", "" | |
| room = game_rooms[room_id_str] | |
| success, msg = room.end_turn(name_str) | |
| turn = f"<div style='background: linear-gradient(135deg, #f39c12, #e67e22); padding: 10px; border-radius: 8px; color: white; text-align: center;'><strong>👑 Giliran:</strong> {room.current_player}</div>" | |
| map_html = room.create_dungeon_map_html() | |
| players = room.get_all_players_html() | |
| logs = room.get_logs_html() | |
| return msg, turn, map_html, players, logs | |
| def refresh_all(room_id_str, name_str): | |
| if not room_id_str or not name_str: | |
| return "", "", "", "" | |
| if room_id_str not in game_rooms: | |
| return "", "", "", "" | |
| room = game_rooms[room_id_str] | |
| status = room.get_player_status(name_str) | |
| turn = f"<div style='background: linear-gradient(135deg, #f39c12, #e67e22); padding: 10px; border-radius: 8px; color: white; text-align: center;'><strong>👑 Giliran:</strong> {room.current_player}</div>" | |
| map_html = room.create_dungeon_map_html() | |
| logs = room.get_logs_html() | |
| return status, turn, map_html, logs | |
| # Event handlers | |
| join_btn.click( | |
| fn=join_room, | |
| inputs=[room_id, player_name], | |
| outputs=[status_box, status_box, map_box, players_box, turn_info, log_box] | |
| ) | |
| move_btn.click( | |
| fn=handle_move, | |
| inputs=[room_id, player_name, move_choice], | |
| outputs=[status_box, status_box, map_box, log_box] | |
| ) | |
| battle_btn.click( | |
| fn=handle_battle, | |
| inputs=[room_id, player_name, battle_choice], | |
| outputs=[status_box, status_box, map_box, log_box] | |
| ) | |
| end_turn_btn.click( | |
| fn=handle_end_turn, | |
| inputs=[room_id, player_name], | |
| outputs=[status_box, turn_info, map_box, players_box, log_box] | |
| ) | |
| refresh_btn.click( | |
| fn=refresh_all, | |
| inputs=[room_id, player_name], | |
| outputs=[status_box, turn_info, map_box, log_box] | |
| ) | |
| return demo | |
| # Create and launch the interface | |
| if __name__ == "__main__": | |
| demo = create_interface() | |
| demo.launch(share=True) |