Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| # ======================================================== | |
| # 1. DATABASE CORE HERO + STATS TREN MPL S17 | |
| # ======================================================== | |
| META_HEROES = { | |
| "Aulus": {"role": "Jungler", "meta": "Sustained DPS", "win_rate": 62.5, "counters": {"Valir": "Zonasi slow.", "Belerick": "Memantulkan pasif ASPD.", "Kaja": "Suppress mutlak."}}, | |
| "Belerick": {"role": "Roamer", "meta": "Anti-Dive/Taunt", "win_rate": 58.3, "counters": {"Karrie": "True damage %HP.", "Lunox": "Penetrasi magic.", "Valir": "Knockback."}}, | |
| "Zhuxin": {"role": "Midlaner", "meta": "Teamfight Control", "win_rate": 54.1, "counters": {"Helcurt": "Silence memutus lentera.", "Natalia": "Ciduk senyap.", "Eudora": "Stun & burst."}}, | |
| "Ling": {"role": "Jungler", "meta": "Dive", "win_rate": 60.2, "counters": {"Minsitthar": "Grounded.", "Khufra": "Bouncing Ball.", "Saber": "Targeting instan."}}, | |
| "Fanny": {"role": "Jungler", "meta": "Dive", "win_rate": 57.8, "counters": {"Khufra": "Menghentikan kabel.", "Kaja": "Suppress.", "Akai": "Kunci dinding."}}, | |
| "Nolan": {"role": "Jungler", "meta": "Fast Clear", "win_rate": 55.4, "counters": {"Khufra": "Counter dash.", "Minsitthar": "Grounded.", "Kaja": "Suppress."}}, | |
| "Hayabusa": {"role": "Jungler", "meta": "Dive/Burst", "win_rate": 59.1, "counters": {"Khufra": "Bouncing ball.", "Saber": "Lock instan.", "Phoveus": "Counter bayangan."}}, | |
| "Hylos": {"role": "Roamer", "meta": "Ube/Regen", "win_rate": 56.7, "counters": {"Karrie": "True damage.", "Valir": "Slow & knockback.", "Lunox": "Penetrasi."}}, | |
| "Tigreal": {"role": "Roamer", "meta": "Wombo Combo", "win_rate": 52.3, "counters": {"Diggie": "Anti-CC masal.", "Akai": "Batalkan ulti.", "Valir": "Knockback."}}, | |
| "Mathilda": {"role": "Roamer", "meta": "Ube/Regen", "win_rate": 55.0, "counters": {"Saber": "Lock terbang.", "Chou": "Tendang jauh.", "Minsitthar": "Grounded."}}, | |
| "Harith": {"role": "Gold Laner", "meta": "Poke/Sustained", "win_rate": 58.9, "counters": {"Minsitthar": "Grounded total.", "Phoveus": "Pasif lompat.", "Khufra": "Bouncing ball."}}, | |
| "Roger": {"role": "Gold Laner", "meta": "Poke/Sustained", "win_rate": 56.4, "counters": {"Grock": "Immune CC.", "Vexana": "Teror lord.", "Terizla": "Sustain."}}, | |
| "Terizla": {"role": "EXP Laner", "meta": "Wombo Combo", "win_rate": 54.8, "counters": {"Valir": "Zonasi dorongan.", "Karrie": "True damage.", "Diggie": "Lepas dari rantai."}}, | |
| "Ruby": {"role": "EXP Laner", "meta": "Sustained", "win_rate": 53.2, "counters": {"Baxia": "Potong lifesteal.", "Phoveus": "Counter dash.", "Minsitthar": "Anti-pasif lompat."}} | |
| } | |
| # ======================================================== | |
| # 2. MASTER POOL HERO MLBB | |
| # ======================================================== | |
| BASE_HEROES_POOL = sorted([ | |
| "Aulus", "Belerick", "Zhuxin", "Ling", "Fanny", "Nolan", "Hayabusa", "Hylos", "Tigreal", "Mathilda", "Harith", "Roger", "Terizla", "Ruby", | |
| "Aamon", "Akai", "Aldous", "Alice", "Alpha", "Alucard", "Angela", "Argus", "Arlott", "Atlas", "Bader", "Badang", "Balmond", "Bane", "Barats", "Baxia", "Beatrix", "Benedetta", "Brody", "Bruno", | |
| "Carmilla", "Cecilion", "Chang'e", "Chou", "Cici", "Claude", "Clint", "Cyclops", "Diggie", "Dyrroth", "Edith", "Esmeralda", "Estes", "Eudora", "Faramis", "Floryn", "Fredrinn", "Freya", | |
| "Gatotkaca", "Gloo", "Gord", "Grock", "Guinevere", "Gusion", "Hanabi", "Hanzo", "Harley", "Helcurt", "Irithel", "Ixia", "Jawhead", "Johnson", "Kadita", "Kagura", "Kaja", "Karina", "Karrie", | |
| "Khaleed", "Khufra", "Kimmy", "Lancelot", "Layla", "Leomord", "Lesley", "Lolita", "Lunox", "Luo Yi", "Martis", "Masha", "Melissa", "Minotaur", "Minsitthar", "Miya", "Moskov", "Nana", "Natalia", | |
| "Natan", "Novaria", "Odette", "Paquito", "Pharsa", "Phoveus", "Popol and Kupa", "Rafaela", "Saber", "Selena", "Silvanna", "Sun", "Thamuz", "Uranus", "Vale", "Valentina", "Valir", "Vexana", | |
| "Wanwan", "X.Borg", "Xavier", "Yi Sun-shin", "Yin", "Yu Zhong", "Yve", "Zilong" | |
| ]) | |
| BLANK_IMG = "https://via.placeholder.com/65x65/0f172a/64748b?text=%3F" | |
| def clean_name(name): | |
| if not name: return "" | |
| # Membersihkan penanda status agar data mentah hero terbaca sistem | |
| return name.replace("⭐ ", "").replace("🔵 ", "").replace("🔴 ", "").replace("❌ ", "").split(" [")[0].strip() | |
| def update_hero_html(hero_name): | |
| if not hero_name: | |
| return f'<div style="display:flex; justify-content:center; margin: 4px 0;"><img src="{BLANK_IMG}" style="width:65px; height:65px; border-radius:8px; border:2px solid #334155;" /></div>' | |
| pure_name = clean_name(hero_name) | |
| url = f"https://via.placeholder.com/65x65/1e293b/f8fafc?text={pure_name[:4]}" | |
| return f'<div style="display:flex; justify-content:center; margin: 4px 0;"><img src="{url}" style="width:65px; height:65px; border-radius:8px; border:2px solid #475569; object-fit:cover;" /></div>' | |
| def make_eval_card(title, items, is_positive=True): | |
| bg_color = "rgba(16, 185, 129, 0.1)" if is_positive else "rgba(239, 68, 68, 0.1)" | |
| border_color = "#10b981" if is_positive else "#ef4444" | |
| title_color = "#34d399" if is_positive else "#f87171" | |
| icon = "✅" if is_positive else "⚠️" | |
| html = f""" | |
| <div style='background: {bg_color}; border: 1px solid {border_color}; padding: 12px; border-radius: 8px; margin-bottom: 10px;'> | |
| <b style='color: {title_color}; font-size: 14px; display: block; margin-bottom: 6px;'>{icon} {title}</b> | |
| <ul style='margin: 0; padding-left: 18px; color: #cbd5e1; font-size: 13px;'> | |
| """ | |
| for item in items: | |
| html += f"<li style='margin-bottom: 4px;'>{item}</li>" | |
| html += "</ul></div>" | |
| return html | |
| def calculate_draft_score(team_list): | |
| score = 50.0 | |
| for h in team_list: | |
| if h in META_HEROES: | |
| score += (META_HEROES[h]["win_rate"] - 50) * 1.5 | |
| return min(max(round(score, 1), 20.0), 98.5) | |
| # ======================================================== | |
| # REFRESH POOL DROPDOWN DENGAN STATUS HERO TERBARU | |
| # ======================================================== | |
| def refresh_all_dropdowns(*all_inputs): | |
| # Memisahkan input berdasarkan perannya | |
| blue_raw = all_inputs[0:5] | |
| red_raw = all_inputs[5:10] | |
| ban_raw = all_inputs[10:20] | |
| blue_picked = [clean_name(x) for x in blue_raw if x] | |
| red_picked = [clean_name(x) for x in red_raw if x] | |
| banned = [clean_name(x) for x in ban_raw if x] | |
| new_pool = [] | |
| for hero in BASE_HEROES_POOL: | |
| star = "⭐ " if hero in META_HEROES else "" | |
| if hero in blue_picked: | |
| new_pool.append(f"🔵 {star}{hero} [TIM KAMU]") | |
| elif hero in red_picked: | |
| new_pool.append(f"🔴 {star}{hero} [LAWAN]") | |
| elif hero in banned: | |
| new_pool.append(f"❌ {star}{hero} [BANNED]") | |
| else: | |
| new_pool.append(f"{star}{hero}") | |
| # Mengembalikan daftar pilihan baru ke seluruh 20 dropdown + 1 format ranked | |
| return [gr.Dropdown(choices=[None] + new_pool, value=x) for x in all_inputs] | |
| # ======================================================== | |
| # CORE CORE EVALUASI TURNAMEN | |
| # ======================================================== | |
| def analyze_tournament_draft(rank, b1, b2, b3, b4, b5, r1, r2, r3, r4, r5, ban1, ban2, ban3, ban4, ban5, ban6, ban7, ban8, ban9, ban10): | |
| blue_team = [clean_name(h) for h in [b1, b2, b3, b4, b5] if h] | |
| red_team = [clean_name(h) for h in [r1, r2, r3, r4, r5] if h] | |
| all_bans = [clean_name(h) for h in [ban1, ban2, ban3, ban4, ban5, ban6, ban7, ban8, ban9, ban10] if h] | |
| blue_pos, blue_neg = [], [] | |
| red_pos, red_neg = [], [] | |
| blue_score = calculate_draft_score(blue_team) | |
| red_score = calculate_draft_score(red_team) | |
| if len(blue_team) < 1: | |
| blue_pos.append("Menunggu rancangan tim...") | |
| else: | |
| blue_pos.append(f"📊 <b>Draft Power Score: {blue_score}%</b>") | |
| for h in blue_team: | |
| if h in META_HEROES: | |
| blue_pos.append(f"<b>{h}</b> (META - WR {META_HEROES[h]['win_rate']}%): Mengamankan laning.") | |
| else: | |
| blue_pos.append(f"<b>{h}</b>: Comfort pick non-prioritas.") | |
| if len(red_team) < 1: | |
| red_pos.append("Menunggu pick lawan...") | |
| else: | |
| red_pos.append(f"📊 <b>Draft Power Score Lawan: {red_score}%</b>") | |
| for h in red_team: | |
| if h in META_HEROES: | |
| red_pos.append(f"<b>{h}</b> (META - WR {META_HEROES[h]['win_rate']}%): Ancaman terdeteksi.") | |
| else: | |
| red_pos.append(f"<b>{h}</b>: Mengisi lane fleksibel.") | |
| # Deteksi Counter | |
| for r_hero in red_team: | |
| if r_hero in META_HEROES: | |
| for c_hero in META_HEROES[r_hero]["counters"]: | |
| if c_hero in blue_team: | |
| blue_neg.append(f"⚠️ <b>{c_hero}</b> milikmu dibatasi ruangnya oleh <b>{r_hero}</b> musuh.") | |
| blue_eval_html = "<div>" + make_eval_card("STRATEGI TIM KAMU", blue_pos, True) + make_eval_card("PERINGATAN DEFENSE", blue_neg, False) + "</div>" | |
| red_eval_html = "<div>" + make_eval_card("ANALISIS KEKUATAN LAWAN", red_pos, True) + make_eval_card("CELAH HERO LAWAN", red_neg, False) + "</div>" | |
| counter_report = "<div style='background-color: #0f172a; padding: 15px; border-radius: 10px; border: 1px solid #334155;'>" | |
| if not red_team: | |
| counter_report += "<p style='color: #94a3b8; margin:0;'>Kalkulasi taktis menunggu pick lawan...</p>" | |
| else: | |
| counter_report += "<h4 style='color:#fbbf24; margin-top:0;'>🛡️ STATUS MONITORING POOL COUNTER:</h4>" | |
| for hero in red_team: | |
| if hero in META_HEROES: | |
| counter_report += f"<p style='color:#cbd5e1; margin: 6px 0;'>Opsi penangkal untuk <b>{hero}</b>: " | |
| counter_options = [] | |
| for c in META_HEROES[hero]["counters"]: | |
| if c in blue_team: | |
| counter_options.append(f"<span style='color:#4ade80;'>{c} [TIM KAMU]</span>") | |
| elif c in red_team: | |
| counter_options.append(f"<span style='color:#ef4444;'>{c} [DICURI LAWAN]</span>") | |
| elif c in all_bans: | |
| counter_options.append(f"<span style='color:#64748b; text-decoration: line-through;'>{c} [BANNED]</span>") | |
| else: | |
| counter_options.append(f"<span style='color:#60a5fa; font-weight:bold;'>{c} [TERSEDIA]</span>") | |
| counter_report += " | ".join(counter_options) + "</p>" | |
| counter_report += "</div>" | |
| return counter_report, blue_eval_html, red_eval_html | |
| # ======================================================== | |
| # INTERFACE DESIGN | |
| # ======================================================== | |
| custom_css = """ | |
| body { background-color: #020617; } | |
| .gradio-container { background-color: #020617 !important; color: white !important; } | |
| .blue-side { border: 2px solid #3b82f6; padding: 15px; border-radius: 12px; background: rgba(59, 130, 246, 0.02); } | |
| .red-side { border: 2px solid #ef4444; padding: 15px; border-radius: 12px; background: rgba(239, 68, 68, 0.02); } | |
| .ban-panel { border: 1px dashed #475569; padding: 12px; border-radius: 10px; background: rgba(15, 23, 42, 0.6); margin-bottom: 15px; } | |
| """ | |
| with gr.Blocks(css=custom_css) as demo: | |
| gr.HTML(""" | |
| <div style='text-align: center; padding: 10px 0;'> | |
| <h1 style='color: #3b82f6; font-size: 26px; margin:0;'>🏆 MLBB HYBRID DRAFT TRACKER</h1> | |
| <p style='color: #64748b; margin: 5px 0;'>Slot Berbasis Role Resmi • Pilihan Hero 100% Fleksibel & Dinamis</p> | |
| </div> | |
| """) | |
| # BAN SECTIONS | |
| with gr.Group(elem_classes="ban-panel"): | |
| gr.HTML("<b style='color: #ef4444; display:block; text-align:center; margin-bottom:5px;'>🚫 TOURNAMENT BAN POOL (FLEKSIBEL)</b>") | |
| with gr.Row(): | |
| ban1 = gr.Dropdown([None] + BASE_HEROES_POOL, label="B-Ban 1") | |
| ban2 = gr.Dropdown([None] + BASE_HEROES_POOL, label="B-Ban 2") | |
| ban3 = gr.Dropdown([None] + BASE_HEROES_POOL, label="B-Ban 3") | |
| ban4 = gr.Dropdown([None] + BASE_HEROES_POOL, label="B-Ban 4") | |
| ban5 = gr.Dropdown([None] + BASE_HEROES_POOL, label="B-Ban 5") | |
| with gr.Row(): | |
| ban6 = gr.Dropdown([None] + BASE_HEROES_POOL, label="R-Ban 1") | |
| ban7 = gr.Dropdown([None] + BASE_HEROES_POOL, label="R-Ban 2") | |
| ban8 = gr.Dropdown([None] + BASE_HEROES_POOL, label="R-Ban 3") | |
| ban9 = gr.Dropdown([None] + BASE_HEROES_POOL, label="R-Ban 4") | |
| ban10 = gr.Dropdown([None] + BASE_HEROES_POOL, label="R-Ban 5") | |
| with gr.Row(): | |
| # BLUE TIM (SLOT BERDASARKAN ROLE, ISI TETAP FULL HERO) | |
| with gr.Column(elem_classes="blue-side", scale=1): | |
| gr.HTML("<h3 style='color: #60a5fa; margin-top:0; text-align:center;'>🔵 TIM KAMU</h3>") | |
| rank_pick = gr.Dropdown(["Pro-Tournament", "Ranked Match"], value="Pro-Tournament", label="Format") | |
| b1 = gr.Dropdown([None] + BASE_HEROES_POOL, label="⚔️ Jungler") | |
| img_b1 = gr.HTML(update_hero_html(None)) | |
| b2 = gr.Dropdown([None] + BASE_HEROES_POOL, label="🛡️ Roamer") | |
| img_b2 = gr.HTML(update_hero_html(None)) | |
| b3 = gr.Dropdown([None] + BASE_HEROES_POOL, label="🔮 Midlaner") | |
| img_b3 = gr.HTML(update_hero_html(None)) | |
| b4 = gr.Dropdown([None] + BASE_HEROES_POOL, label="⚔️ EXP Laner") | |
| img_b4 = gr.HTML(update_hero_html(None)) | |
| b5 = gr.Dropdown([None] + BASE_HEROES_POOL, label="🏹 Gold Laner") | |
| img_b5 = gr.HTML(update_hero_html(None)) | |
| # CENTER CARD EVALUATION BLOCKS | |
| with gr.Column(scale=1.2): | |
| btn_analyze = gr.Button("⚡ JALANKAN TRACER & EVALUASI DRAF", variant="primary", size="lg") | |
| gr.HTML("<h3 style='color: #3b82f6; margin-top:15px; text-align:center;'>📊 CANDLE EVALUASI TIM KAMU</h3>") | |
| blue_eval_out = gr.HTML("<p style='color: #64748b;'>Menyusun tracing data...</p>") | |
| gr.HTML("<h3 style='color: #ef4444; margin-top:15px; text-align:center;'>📊 CANDLE EVALUASI TIM LAWAN</h3>") | |
| red_eval_out = gr.HTML("<p style='color: #64748b;'>Menyusun tracing data...</p>") | |
| # RED TIM (SLOT BERDASARKAN ROLE, ISI TETAP FULL HERO) | |
| with gr.Column(elem_classes="red-side", scale=1): | |
| gr.HTML("<h3 style='color: #f87171; margin-top:0; text-align:center;'>🔴 TIM LAWAN</h3>") | |
| r1 = gr.Dropdown([None] + BASE_HEROES_POOL, label="⚔️ Enemy Jungler") | |
| img_r1 = gr.HTML(update_hero_html(None)) | |
| r2 = gr.Dropdown([None] + BASE_HEROES_POOL, label="🛡️ Enemy Roamer") | |
| img_r2 = gr.HTML(update_hero_html(None)) | |
| r3 = gr.Dropdown([None] + BASE_HEROES_POOL, label="🔮 Enemy Midlaner") | |
| img_r3 = gr.HTML(update_hero_html(None)) | |
| r4 = gr.Dropdown([None] + BASE_HEROES_POOL, label="⚔️ Enemy EXP Laner") | |
| img_r4 = gr.HTML(update_hero_html(None)) | |
| r5 = gr.Dropdown([None] + BASE_HEROES_POOL, label="🏹 Enemy Gold Laner") | |
| img_r5 = gr.HTML(update_hero_html(None)) | |
| gr.HTML("<h2 style='color: #fbbf24; border-bottom: 2px solid #fbbf24; padding-bottom: 5px; margin-top:20px;'>🕵️ POOL COUNTER REAL-TIME MONITORING ADVICE</h2>") | |
| counter_out = gr.HTML("<p style='color: #64748b;'>Engine pelacak status siap dijalankan...</p>") | |
| # KUMPULAN INPUT UNTUK DETEKSI STATE DROP DOWN | |
| all_dropdown_components = [b1, b2, b3, b4, b5, r1, r2, r3, r4, r5, ban1, ban2, ban3, ban4, ban5, ban6, ban7, ban8, ban9, ban10] | |
| # TRACER DYNAMIC REFRESH: Jika ada dropdown berubah, semua dropdown dapat update teks tanda status! | |
| for comp in all_dropdown_components: | |
| comp.change( | |
| fn=refresh_all_dropdowns, | |
| inputs=all_dropdown_components, | |
| outputs=all_dropdown_components | |
| ) | |
| # BINDING IMAGE CHANGES | |
| b1.change(fn=update_hero_html, inputs=b1, outputs=img_b1) | |
| b2.change(fn=update_hero_html, inputs=b2, outputs=img_b2) | |
| b3.change(fn=update_hero_html, inputs=b3, outputs=img_b3) | |
| b4.change(fn=update_hero_html, inputs=b4, outputs=img_b4) | |
| b5.change(fn=update_hero_html, inputs=b5, outputs=img_b5) | |
| r1.change(fn=update_hero_html, inputs=r1, outputs=img_r1) | |
| r2.change(fn=update_hero_html, inputs=r2, outputs=img_r2) | |
| r3.change(fn=update_hero_html, inputs=r3, outputs=img_r3) | |
| r4.change(fn=update_hero_html, inputs=r4, outputs=img_r4) | |
| r5.change(fn=update_hero_html, inputs=r5, outputs=img_r5) | |
| # ACTION BUTTON | |
| btn_analyze.click( | |
| fn=analyze_tournament_draft, | |
| inputs=[rank_pick] + all_dropdown_components, | |
| outputs=[counter_out, blue_eval_out, red_eval_out] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(show_error=True) |