tar / app.py
CC2311's picture
Update app.py
a2852b2 verified
raw
history blame
76.2 kB
from __future__ import annotations
import os, json, math, itertools, tempfile, hashlib, copy
from datetime import datetime
from functools import lru_cache
from typing import Any, Dict, List, Tuple, Optional, Set, Generator
import pandas as pd
import gradio as gr
try:
from zoneinfo import ZoneInfo
KST = ZoneInfo("Asia/Seoul")
except Exception:
KST = None
def _now():
return datetime.now(KST) if KST else datetime.utcnow()
_VIS_DB = os.path.join(tempfile.gettempdir(), "visits_daily.json")
_VIS_SALT = os.environ.get("VIS_SALT", "SALT")
def _client_ip(req: gr.Request | None) -> str:
try:
if req is None:
return "?"
for k in ("x-forwarded-for","X-Forwarded-For","x-real-ip","X-Real-Ip"):
v = req.headers.get(k)
if v:
return v.split(",")[0].strip()
return getattr(req.client, "host", "") or "?"
except Exception:
return "?"
def _ip_key(req: gr.Request | None) -> str:
try:
return hashlib.sha256((_VIS_SALT + _client_ip(req)).encode()).hexdigest()
except Exception:
return hashlib.sha256((_VIS_SALT + "anon").encode()).hexdigest()
def _vload():
try:
with open(_VIS_DB,"r",encoding="utf-8") as f:
return json.load(f)
except Exception:
return {}
def _vsave(d:dict):
with open(_VIS_DB,"w",encoding="utf-8") as f:
json.dump(d,f)
def register_unique_visit(request: gr.Request | None = None) -> str:
d = _vload()
key = _now().strftime("%Y-%m-%d")
if key not in d:
d = {key:{}}
ipk = _ip_key(request)
if ipk not in d[key]:
d[key][ipk] = int(_now().timestamp())
_vsave(d)
return f"<div style='text-align:right;font-size:12px;'>오늘 방문자: <b>{len(d[key])}</b></div>"
# =============================================================================
# 외부 모듈 폴백
# =============================================================================
try:
from calc.type import get_base_stats as _get_base_stats_raw
from calc.awakening import get_awakening_stat as _get_awakening_stat_raw
from calc.spirit import spirit_breakdown as _spirit_breakdown_raw
from calc.collection import apply_collection as _apply_collection_raw
from calc.potion import apply_potion as _apply_potion_raw
import calc.accessory as accmod
from calc.buff import BUFFS_ALL as _BUFFS_ALL
from calc.gem import GEM_DISTS as _GEM_DISTS
except Exception:
def _get_base_stats_raw(typ:str)->Dict[str,int]:
base = {
"체":{"hp":400,"atk":100,"def":100},
"공":{"hp":200,"atk":200,"def":100},
"방":{"hp":200,"atk":100,"def":200},
"체공":{"hp":350,"atk":150,"def":100},
"체방":{"hp":350,"atk":100,"def":150},
"공방":{"hp":200,"atk":150,"def":150},
}
t = typ.replace("(진각)","")
return base.get(t, {"hp":300,"atk":120,"def":120})
def _get_awakening_stat_raw(typ:str)->Dict[str,int]:
return {"hp":60,"atk":20,"def":20}
def _spirit_breakdown_raw(arr:List[dict]):
pct = {"hp":0.15,"atk":0.10,"def":0.10}
flat = {"hp":120,"atk":40,"def":40}
sub = {"hp":0,"atk":0,"def":0}
return pct, flat, sub
def _apply_collection_raw(_=None):
return {"hp":0,"atk":0,"def":0}
def _apply_potion_raw(_=None):
return {"hp":0,"atk":0,"def":0}
class _AccStub:
df_acc = pd.DataFrame([
{"lv":19,"이름":"황보","hp%":0.05,"atk%":0.05,"def%":0.05},
{"lv":19,"이름":"악보","hp%":0.06,"atk%":0.0,"def%":0.06},
])
accmod = _AccStub()
_BUFFS_ALL = {
"0벞":{"hp":0.0,"atk":0.0,"def":0.0},
"HP20%":{"hp":0.2,"atk":0.0,"def":0.0},
"ATK20%":{"hp":0.0,"atk":0.2,"def":0.0},
"DEF20%":{"hp":0.0,"atk":0.0,"def":0.2},
"HP40%":{"hp":0.4,"atk":0.0,"def":0.0},
"ATK40%":{"hp":0.0,"atk":0.4,"def":0.0},
"DEF40%":{"hp":0.0,"atk":0.0,"def":0.4},
"HP+ATK20%":{"hp":0.2,"atk":0.2,"def":0.0},
"HP+DEF20%":{"hp":0.2,"atk":0.0,"def":0.2},
"ATK+DEF20%":{"hp":0.0,"atk":0.2,"def":0.2},
}
_GEM_DISTS = [
(5,0,0),(0,5,0),(0,0,5),
(4,1,0),(4,0,1),(1,4,0),(0,4,1),(1,0,4),(0,1,4),
(3,2,0),(3,0,2),(2,3,0),(0,3,2),(2,0,3),(0,2,3)
]
BUFFS_ALL: Dict[str,Dict[str,float]] = dict(_BUFFS_ALL)
GEM_DISTS: List[Tuple[int,int,int]] = list(_GEM_DISTS)
try:
from tar_denom_db import get_denom_from_db as _fast_denom_get
except Exception:
def _fast_denom_get(mode:int, typ:str, buff:str)->int:
return 0
try:
import spec_db
except Exception:
class _SpecDBStub:
def get_M(self, profile:int, typ:str, buff_label:str)->float|None:
return None
def profile_name(self, p:int)->str:
return {1:"7.0/펜던트X",2:"9.0/펜던트O"}.get(p,str(p))
spec_db = _SpecDBStub()
TYPES_ALL = ["체","공","방","체공","체방","공방",
"(진각)체","(진각)공","(진각)방","(진각)체공","(진각)체방","(진각)공방"]
BASE_TYPES = ["체","공","방","체공","체방","공방"]
TWO_BUFFS = ["HP40%","ATK40%","DEF40%","HP+ATK20%","HP+DEF20%","ATK+DEF20%"]
ONE_20 = ["HP20%","ATK20%","DEF20%"]
ALL_BUFF_CHOICES = TWO_BUFFS + ONE_20 + ["0벞"]
ENCH_LIST = [
("HP", {"hp":0.21,"atk":0.0,"def":0.0}),
("ATK",{"hp":0.0,"atk":0.21,"def":0.0}),
("DEF",{"hp":0.0,"atk":0.0,"def":0.21}),
]
ENCH_DICT = dict(ENCH_LIST)
# =============================================================================
# 보조
# =============================================================================
def strip_jingak(t:str)->str:
return t.replace("(진각)","") if isinstance(t,str) else t
@lru_cache(maxsize=None)
def _get_base_stats_cached(t:str)->Dict[str,int]:
return dict(_get_base_stats_raw(t))
@lru_cache(maxsize=None)
def _get_awakening_stat_cached(t:str)->Dict[str,int]:
return dict(_get_awakening_stat_raw(t))
def _sp_key_for_cache(sp:List[dict])->Tuple:
if not sp: return ()
return tuple((int(d.get("slot",0)),str(d.get("stat","")),str(d.get("type",""))) for d in sp)
@lru_cache(maxsize=None)
def _spirit_breakdown_cached(key:Tuple)->Tuple[Dict[str,float],Dict[str,int],Dict[str,int]]:
if not key:
zero_pct = {"hp":0.0,"atk":0.0,"def":0.0}
zero_flat = {"hp":0,"atk":0,"def":0}
zero_sub = {"hp":0,"atk":0,"def":0}
return zero_pct, zero_flat, zero_sub
arr = [{"slot":s,"stat":st,"type":tp} for (s,st,tp) in key]
return _spirit_breakdown_raw(arr)
_POTION = _apply_potion_raw({})
_COLLECTION = _apply_collection_raw({"hp":0,"atk":0,"def":0})
def acc_label(r:dict|None)->str:
if not r: return "-"
lv = int(r.get("lv",r.get("레벨",r.get("level",0))))
name = str(r.get("이름","")).strip()
return f"{lv} {name}".strip()
def acc_row_by_label(tag:str)->dict|None:
try:
df = accmod.df_acc.copy()
except Exception:
return None
s = str(tag).strip()
parts = s.split()
lv, name = None, s
try:
lv = int(parts[0]); name = " ".join(parts[1:]).strip()
except Exception:
pass
try:
if lv is not None:
sub = df[(df["lv"].astype(int)==lv)&(df["이름"].astype(str)==name)]
else:
sub = df[df["이름"].astype(str)==name]
if not sub.empty:
return sub.iloc[0].to_dict()
except Exception:
return None
return None
def accessory_names_by_levels(levels:List[int])->List[str]:
try:
df = accmod.df_acc.copy()
df = df[df["lv"].astype(int).isin([int(x) for x in (levels or [])])]
names = df[["lv","이름"]].astype({"lv":int,"이름":str}).sort_values(["lv","이름"])
return [f"{lv} {name}" for lv,name in names.values.tolist()]
except Exception:
return []
def _pct_from(x:Any)->Dict[str,float]:
if isinstance(x,dict):
return {"hp":float(x.get("hp",0.0)),"atk":float(x.get("atk",0.0)),"def":float(x.get("def",0.0))}
return dict(BUFFS_ALL.get(str(x), {"hp":0,"atk":0,"def":0}))
def tar_percent(B:float, M:float)->float:
try:
B = float(B); M = float(M)
except Exception:
return 0.0
if M <= 0: return 0.0
r = B/M
if r <= 1.0:
return round(max(0.0, 200.0*r - 100.0), 4)
return round(100.0*r, 4)
def make_gild_pct(hp, atk, df):
def f(v):
try: return float(v)/100.0
except Exception: return 0.0
return {"hp":f(hp),"atk":f(atk),"def":f(df)}
# 등급 보정
GRADE_INCR_PER_1 = {
"체":{"hp":80,"atk":0,"def":0},
"공":{"hp":0,"atk":20,"def":0},
"방":{"hp":0,"atk":0,"def":20},
"체방":{"hp":40,"atk":0,"def":10},
"체공":{"hp":40,"atk":10,"def":0},
"공방":{"hp":0,"atk":10,"def":10},
}
@lru_cache(maxsize=1024)
def grade_boost_for_type_precise_tuple(typ:str, grade_str:str)->Tuple[int,int,int]:
t = strip_jingak(typ)
g = float(str(grade_str))
delta = max(0.0, g - 7.0)
inc = GRADE_INCR_PER_1.get(t, {"hp":0,"atk":0,"def":0})
return (int(round(inc["hp"]*delta)), int(round(inc["atk"]*delta)), int(round(inc["def"]*delta)))
def grade_boost_for_type_precise(typ:str, grade_str:str)->Dict[str,int]:
h,a,d = grade_boost_for_type_precise_tuple(typ, grade_str)
return {"hp":h, "atk":a, "def":d}
# =============================================================================
# 계산 코어
# =============================================================================
def _floor_int(x:float)->int:
return int(math.floor(float(x)))
def compute_final_stat_and_bib(
typ:str,
ench_pct:Dict[str,float],
acc_row:dict|None,
gem_sums:Tuple[float,float,float], # (hpSum, atkSum, defSum)
spirits_spec:List[dict],
pendant_pct:Dict[str,float]|None,
buff_label_or_pct:Dict[str,float]|str,
nerf_label_or_pct:Dict[str,float]|str,
grade_sel:str,
base_stat_manual:Dict[str,int]|None=None,
jingak:bool=False
)->Tuple[int, Dict[str,int]]:
bs = _get_base_stats_cached(typ if base_stat_manual is None else strip_jingak(typ)) if base_stat_manual is None else dict(base_stat_manual)
aw = _get_awakening_stat_cached(typ) if (jingak and base_stat_manual is None) else {"hp":0,"atk":0,"def":0}
boost_h, boost_a, boost_d = grade_boost_for_type_precise_tuple(typ, grade_sel)
C1 = {"hp": bs["hp"] + aw["hp"] + boost_h, "atk": bs["atk"] + aw["atk"] + boost_a, "def": bs["def"] + aw["def"] + boost_d}
C2 = {"hp":gem_sums[0]*4, "atk":gem_sums[1], "def":gem_sums[2]}
base_total = {k: C1[k] + C2[k] for k in C1}
base_total = {k: base_total[k] + _POTION.get(k,0) for k in base_total}
hp_pct = float((acc_row or {}).get("hp%",0))/100.0
atk_pct = float((acc_row or {}).get("atk%",0))/100.0
def_pct = float((acc_row or {}).get("def%",0))/100.0
ench = ench_pct or {"hp":0,"atk":0,"def":0}
mul1 = {"hp":1+hp_pct+ench.get("hp",0.0),
"atk":1+atk_pct+ench.get("atk",0.0),
"def":1+def_pct+ench.get("def",0.0)}
st1 = {k:_floor_int(base_total[k]*mul1[k]) for k in ("hp","atk","def")}
pct7, flat8, sub9 = _spirit_breakdown_cached(_sp_key_for_cache(spirits_spec))
st2 = {k: _floor_int(st1[k]*(1+pct7[k]) + flat8[k]*(1+pct7[k])) for k in ("hp","atk","def")}
if pendant_pct:
st2 = {k: _floor_int(st2[k]*(1.0+float(pendant_pct.get(k,0.0)))) for k in st2}
st3 = {k: st2[k] + _COLLECTION.get(k,0) + sub9[k] for k in ("hp","atk","def")}
bs_for_buff = {"hp": bs["hp"] + boost_h, "atk": bs["atk"] + boost_a, "def": bs["def"] + boost_d}
buff = _pct_from(buff_label_or_pct)
nerf = _pct_from(nerf_label_or_pct)
eff = {"hp":buff["hp"]-nerf["hp"], "atk":buff["atk"]-nerf["atk"], "def":buff["def"]-nerf["def"]}
add_buff = {k: _floor_int(bs_for_buff[k]*eff[k]) for k in ("hp","atk","def")}
final = {k: st3[k] + add_buff[k] for k in ("hp","atk","def")}
bib = final["hp"] * final["atk"] * final["def"]
return int(bib), final
# =============================================================================
# SPEC 분모
# =============================================================================
def _fast_denom_or_none(profile:int, typ:str, buff_label:str)->float|None:
if int(profile) not in (1,2): return None
if strip_jingak(typ) not in BASE_TYPES: return None
if buff_label not in TWO_BUFFS: return None
try:
v = _fast_denom_get(profile, strip_jingak(typ), buff_label)
if int(v) > 0: return float(v)
except Exception:
pass
return None
def get_plain_M(profile:int, typ:str, label:str)->float:
fast = _fast_denom_or_none(profile, typ, label)
if fast is not None:
return float(fast)
try:
m = spec_db.get_M(profile, strip_jingak(typ), label)
return float(m) if m is not None else 0.0
except Exception:
return 0.0
# =============================================================================
# 젬/정령/펜던트 보조
# =============================================================================
def _compact_used_gems(items:List[str])->str:
order = {"체":0,"공":1,"방":2}
arr = []
for s in items:
s = s.strip()
if not s: continue
arr.append((order.get(s[0],9), s, int("".join(ch for ch in s[1:] if ch.isdigit()) or "0")))
arr.sort(key=lambda x:(x[0], -x[2]))
return " ".join(s for _,s,_ in arr)
def spirit_default_row()->dict:
return {
"사용": False,
"귀속": "정령 귀속X",
"1옵 스탯": "체력", "1옵 모드": "%",
"2옵 스탯": "공격력", "2옵 모드": "%",
"3옵 스탯": "방어력", "3옵 모드": "%",
"4옵 스탯": "방어력", "4옵 모드": "+",
"부가옵": "체력40",
}
def make_spirit_obj(row:dict)->List[dict]:
return [
{"slot":1,"stat":row["1옵 스탯"],"type":row["1옵 모드"]},
{"slot":2,"stat":row["2옵 스탯"],"type":row["2옵 모드"]},
{"slot":3,"stat":row["3옵 스탯"],"type":row["3옵 모드"]},
{"slot":4,"stat":row["4옵 스탯"],"type":row["4옵 모드"]},
{"slot":5,"stat":row["부가옵"],"type":"부가옵"},
]
def format_spirit_label(sp:dict|List[dict])->str:
s = sp if isinstance(sp,list) else sp["spec"]
def one(slot):
st = s[slot-1]
if st["slot"]==5:
return st["stat"]
unit = "%" if st["type"]=="%" else "+"
return f'{st["stat"][0]}{unit}'
return f'{one(1)}/{one(2)}/{one(3)}/{one(4)}/{s[4]["stat"]}'
def gem_inventory_default()->pd.DataFrame:
return pd.DataFrame([{"수치":v,"체":0,"공":0,"방":0} for v in [34,35,36,37,38,39,40]])
def _as_int_safe(x, default=0):
try:
if x is None or (isinstance(x,float) and math.isnan(x)):
return default
return int(float(x))
except Exception:
return default
# -------------------------------------------------------------------------
# 🚀 최적화된 GemPool 클래스 (Dict 대신 List 사용)
# -------------------------------------------------------------------------
class GemPool:
def __init__(self, df: pd.DataFrame = None, _internal_data=None):
if _internal_data is not None:
self.data = _internal_data
self.valid = True
return
self.data = [[0]*7, [0]*7, [0]*7]
self.valid = False
if df is None or df.empty:
return
col_map = {"수치": None, "체": None, "공": None, "방": None}
for k in list(col_map.keys()):
for cand in [k, k.strip(), str(k)]:
if cand in df.columns:
col_map[k] = cand
break
if not all(col_map.values()):
return
for _, r in df.iterrows():
try:
v = int(float(r[col_map["수치"]]))
except: continue
if v < 34 or v > 40: continue
idx = v - 34
def get_val(col):
val = r.get(col, 0)
try: return int(float(0 if (val is None or (isinstance(val,str) and not val.strip())) else val))
except: return 0
self.data[0][idx] += get_val(col_map["체"])
self.data[1][idx] += get_val(col_map["공"])
self.data[2][idx] += get_val(col_map["방"])
self.valid = True
def total_count(self):
return sum(sum(arr) for arr in self.data)
def check_feasibility(self, dist_list: List[Tuple[int,int,int]]) -> List[Tuple[int,int,int]]:
h_sum = sum(self.data[0])
a_sum = sum(self.data[1])
d_sum = sum(self.data[2])
out = []
for d in dist_list:
if h_sum >= d[0] and a_sum >= d[1] and d_sum >= d[2] and sum(d)==5:
out.append(d)
return out
def allocate_fast(self, dist: Tuple[int,int,int]) -> Tuple[bool, Tuple[float,float,float], List[Tuple[int,int]]]:
sums = [0.0, 0.0, 0.0]
rollback_info = []
for stat_idx in range(3):
need = dist[stat_idx]
if need == 0: continue
arr = self.data[stat_idx]
current_sum = 0
for i in range(6, -1, -1):
if need <= 0: break
count = arr[i]
if count > 0:
take = min(need, count)
arr[i] -= take
need -= take
val = i + 34
current_sum += take * val
rollback_info.append((stat_idx, i, take))
if need > 0:
self.restore(rollback_info)
return False, (0,0,0), []
sums[stat_idx] = float(current_sum)
return True, tuple(sums), rollback_info
def restore(self, rollback_info: List[Tuple[int,int]]):
for stat_idx, val_idx, count in rollback_info:
self.data[stat_idx][val_idx] += count
def generate_used_gem_string(self, rollback_info: List[Tuple[int,int]]) -> List[str]:
res = []
labels = ["체", "공", "방"]
for stat_idx, val_idx, count in rollback_info:
name = f"{labels[stat_idx]}{val_idx + 34}"
res.extend([name] * count)
return res
def copy_fast(self):
new_data = [arr[:] for arr in self.data]
return GemPool(None, _internal_data=new_data)
def get_optimistic_sum(self, stat_idx: int, count: int) -> float:
if count <= 0: return 0.0
arr = self.data[stat_idx]
total_val = 0.0
taken = 0
for idx in range(6, -1, -1):
if taken >= count: break
available = arr[idx]
if available > 0:
grab = min(count - taken, available)
val = idx + 34
total_val += grab * val
taken += grab
return total_val
def gem_inventory_to_pool(df: pd.DataFrame) -> GemPool:
return GemPool(df)
def _feasible_dists_by_count(pool: GemPool) -> List[Tuple[int,int,int]]:
return pool.check_feasibility(GEM_DISTS)
def allocate_gems_for_dist(pool: GemPool, dist: Tuple[int,int,int]):
ok, sums, rb = pool.allocate_fast(dist)
if not ok: return False, {}, (0,0,0), []
return True, {}, sums, []
# =============================================================================
# [수정] 펜던트 로직 최적화 (이 부분을 기존 펜던트 함수들 대신 덮어씌우세요)
# =============================================================================
@lru_cache(maxsize=1024)
def _get_partitions(total: int, num_slots: int, max_val: int = 6) -> List[Tuple[int, ...]]:
"""
정해진 합(total)을 num_slots개의 숫자로 나누는 모든 경우의 수 (순서 유의미)
각 슬롯의 최대값은 max_val(6)
"""
if num_slots == 1:
if 0 <= total <= max_val:
return [(total,)]
return []
res = []
# 첫 슬롯에 들어갈 수 있는 값의 범위 계산 (가지치기)
min_current = max(0, total - (num_slots - 1) * max_val)
max_current = min(total, max_val)
for i in range(min_current, max_current + 1):
for p in _get_partitions(total - i, num_slots - 1, max_val):
res.append((i,) + p)
return res
@lru_cache(maxsize=256)
def enumerate_pendants(total: int, grade: str) -> List[Dict[str, float]]:
"""
펜던트 조합 생성기 (캐싱 및 DP 적용으로 속도 대폭 개선)
"""
grade_to_lines = {"별": 1, "달": 2, "태양": 3}
L = grade_to_lines.get(grade, 3)
# 유효 범위 체크 (최소 0, 최대 6 * 줄 수)
total = max(0, min(int(total), 6 * L))
if total <= 0:
return [None] # 0%일 경우
# 1. 숫자를 나누는 경우의 수만 먼저 구함 (매우 빠름)
partitions = _get_partitions(total, L, 6)
unique_stats = set()
result = []
# 2. 구해진 숫자 조합에 스탯(체/공/방) 속성 부여
stat_types = ["hp", "atk", "def"]
# 중복 계산 방지
for stat_comb in itertools.product(stat_types, repeat=L):
for part in partitions:
current_stat = {"hp": 0, "atk": 0, "def": 0}
for val, s_type in zip(part, stat_comb):
current_stat[s_type] += val
# 튜플로 변환하여 중복 제거 (Set 활용)
stat_tuple = (current_stat["hp"], current_stat["atk"], current_stat["def"])
if stat_tuple not in unique_stats:
unique_stats.add(stat_tuple)
result.append({k: v / 100.0 for k, v in current_stat.items()})
return result or [None]
def pendant_label(pct:Dict[str,float]|None)->str:
if not pct: return "0/0/0"
return f'{int(round(pct.get("hp",0)*100))}/{int(round(pct.get("atk",0)*100))}/{int(round(pct.get("def",0)*100))}'
# =============================================================================
# 즐겨찾기(롤백: 단일 슬롯)
# =============================================================================
_FAV_DB = os.path.join(os.path.dirname(__file__), "tar_fav.json")
def _fav_load():
try:
with open(_FAV_DB,"r",encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, dict):
return {}
if "_global" in data:
data.pop("_global", None)
return data
except Exception:
return {}
def _fav_save(d:dict):
tmp = _FAV_DB + ".tmp"
with open(tmp,"w",encoding="utf-8") as f:
json.dump(d,f,ensure_ascii=False)
f.flush(); os.fsync(f.fileno())
os.replace(tmp,_FAV_DB)
def fav_save_generic(keyspace:str, payload:dict, request:gr.Request|None):
d = _fav_load()
d.pop("_global", None)
ipk = _ip_key(request)
ts = int(_now().timestamp())
per_ip = d.get(ipk, {})
per_ip[keyspace] = {"ts": ts, "data": payload}
d[ipk] = per_ip
_fav_save(d)
return "<span style='color:green;'>즐겨찾기 저장 완료</span>"
def fav_load_generic(keyspace:str, request:gr.Request|None)->dict:
d = _fav_load()
ipk = _ip_key(request)
obj = ((d.get(ipk) or {}).get(keyspace) or {}).get("data")
return obj if isinstance(obj, dict) else {}
# =============================================================================
# 자동 계산기
# =============================================================================
def resolve_row_buff_list(mode:str)->List[Tuple[str,Dict[str,float]]]:
if mode=="최적화":
return [(k,v) for k,v in BUFFS_ALL.items() if k!="0벞"]
if mode=="1벞 최적화":
keys=["HP20%","ATK20%","DEF20%"]
return [(k, BUFFS_ALL[k]) for k in keys if k in BUFFS_ALL]
if mode=="0벞":
return [("0벞",BUFFS_ALL["0벞"])]
return [(mode, BUFFS_ALL.get(mode, {"hp":0,"atk":0,"def":0}))]
def auto_run(
types_sel, gem_vals_sel,
acc_packed,
s1,m1,s2,m2,s3,m3,s4,m4,subopt,sp_skip,
buff_mode, nerf_mode, grade_sel,
_unused,
pnd_opt_on, pnd_total, pnd_grade,
pnd_manual_on, pnd_manual_entries,
dedup_type, dedup_buff, dedup_acc, dedup_ench, dedup_sp,
topn, sort_key
)->pd.DataFrame:
try:
types = BASE_TYPES[:] if not types_sel else types_sel[:]
gem_vals = [int(x) for x in (gem_vals_sel or [str(v) for v in range(34,41)])]
acc_rows=[]
for tag,use,hp,atk,df in acc_packed or []:
if not bool(use): continue
base = acc_row_by_label(tag)
if not base: continue
flags=[]
if bool(hp): flags.append(("HP",ENCH_DICT["HP"]))
if bool(atk): flags.append(("ATK",ENCH_DICT["ATK"]))
if bool(df): flags.append(("DEF",ENCH_DICT["DEF"]))
if not flags: flags = ENCH_LIST[:]
for nm,pct in flags:
acc_rows.append((base,nm,pct))
if not acc_rows:
for n in accessory_names_by_levels([19]):
base=acc_row_by_label(n)
if base: acc_rows.append((base,"HP",ENCH_DICT["HP"]))
spirit = [] if bool(sp_skip) else [
{"slot":1,"stat":s1,"type":m1},
{"slot":2,"stat":s2,"type":m2},
{"slot":3,"stat":s3,"type":m3},
{"slot":4,"stat":s4,"type":m4},
{"slot":5,"stat":subopt,"type":"부가옵"},
]
buff_items = resolve_row_buff_list(buff_mode)
pnd_auto_list = enumerate_pendants(pnd_total if pnd_opt_on else 0, pnd_grade)
pnd_manual_cands: List[Dict[str,float]] = []
if pnd_manual_on:
for ent in pnd_manual_entries or []:
if not ent.get("use"): continue
lines=[(ent.get("l1s"), ent.get("l1v",0)),
(ent.get("l2s"), ent.get("l2v",0)),
(ent.get("l3s"), ent.get("l3v",0))]
accp={"hp":0,"atk":0,"def":0}
for st,v in lines:
st2={"체력":"hp","공격력":"atk","방어력":"def"}.get(st)
v=max(0,min(6,int(v or 0)))
if st2: accp[st2]+=v
if accp["hp"]+accp["atk"]+accp["def"]>0:
pnd_manual_cands.append({k:accp[k]/100.0 for k in accp})
rows=[]
for typ in types:
base_manual = None if "(진각)" in typ else _get_base_stats_cached(strip_jingak(typ))
jingak = "(진각)" in typ
for gv in gem_vals:
for dist in GEM_DISTS:
if sum(dist)!=5: continue
gem_sums = (dist[0]*gv, dist[1]*gv, dist[2]*gv)
for (acc_row,ench_name,ench_pct) in acc_rows:
pendants = (pnd_manual_cands if (pnd_manual_on and pnd_manual_cands) else pnd_auto_list)
for buf_label,_pct in buff_items:
for pnd_pct in pendants:
bib, final = compute_final_stat_and_bib(
typ, ench_pct, acc_row, gem_sums,
spirit, pnd_pct, buf_label, nerf_mode,
grade_sel, base_stat_manual=base_manual, jingak=jingak
)
M = get_plain_M(2, strip_jingak(typ), buf_label if buf_label in TWO_BUFFS else "HP40%")
TAR = tar_percent(bib, M)
rows.append({
"타입":typ, "버프":buf_label, "너프":nerf_mode,
"펜던트": pendant_label(pnd_pct),
"젬수치":gv, "젬분배":f"{dist[0]}/{dist[1]}/{dist[2]}",
"장신구":acc_label(acc_row), "인첸트":ench_name,
"정령": ("미사용" if not spirit else format_spirit_label(spirit)),
"비벨":int(bib), "TAR%":TAR,
"HP":final["hp"], "ATK":final["atk"], "DEF":final["def"],
})
if not rows: return pd.DataFrame()
df=pd.DataFrame(rows)
if sort_key=="TAR%":
df=df.sort_values(["TAR%","비벨"],ascending=False)
else:
df=df.sort_values(["비벨","TAR%"],ascending=False)
keys=[]
if dedup_type: keys.append("타입")
if dedup_buff: keys.append("버프")
if dedup_acc: keys.append("장신구")
if dedup_ench: keys.append("인첸트")
if dedup_sp: keys.append("정령")
if keys: df=df.drop_duplicates(subset=keys, keep="first")
return df.reset_index(drop=True).head(int(topn))
except Exception as e:
return pd.DataFrame({"오류":[str(e)]})
# =============================================================================
# 길드전 최적화 (GemPool & FastCandidate 적용)
# =============================================================================
CSS = """
footer, #footer, .theme-toggle {display:none !important;}
.gradio-container {max-width: 1220px !important; margin: 0 auto !important;}
#guild_table_df table td:nth-child(1), #guild_table_df table th:nth-child(1){ width:44px !important; text-align:center; }
"""
FOCUS_KILL_JS = """
<script>
(function(){
var T=200, until=0;
function now(){return Date.now();}
function isTab(el){ if(!el) return false; var t=(el.tagName||'').toLowerCase(); return t==='summary'||(el.closest && el.closest('summary')); }
document.addEventListener('pointerdown', e=>{ if(isTab(e.target)) until=now()+T;}, true);
document.addEventListener('focusout', e=>{ if(now()<until) e.stopPropagation(); }, true);
})();
</script>
"""
NUM_DECKS = 3
def _score_tuple(sel:List[dict], goal:str)->Tuple[float,float]:
tars = sum(c["T"] for c in sel)/NUM_DECKS
bibs = sum(c["bib"] for c in sel)/NUM_DECKS
if goal=="TAR우선": return (tars, bibs)
return (bibs, tars) # 비벨우선
# -------------------------------------------------------------------------
# 🚀 FastCandidate: 루프 내 계산 최소화 클래스
# -------------------------------------------------------------------------
class FastCandidate:
__slots__ = (
"slot", "typ", "buff", "nerf", "grade",
"acc_key", "acc_row", "allowed", "no_enchant",
"sp", "sp_label",
"dist", "pnd", "pnd_id",
"c1", "pct7", "flat8", "sub9", "eff", "bs_buff",
"acc_hp_pct", "acc_atk_pct", "acc_def_pct",
"pnd_mul", "coll_add", "potion_add",
"approx_bib", "approx_tar"
)
def __init__(self, slot, typ, buff, nerf, grade,
acc_key, acc_row, allowed, no_enchant,
sp, dist, pnd, pnd_id, M_val,
optimistic_sums: Tuple[float,float,float]): # [수정] max_gem_vals 대신 optimistic_sums 사용
self.slot = slot
self.typ = typ
self.buff = buff
self.nerf = nerf
self.grade = grade
self.acc_key = acc_key
self.acc_row = acc_row
self.allowed = allowed
self.no_enchant = no_enchant
self.sp = sp
self.sp_label = format_spirit_label(sp["spec"])
self.dist = dist
self.pnd = pnd
self.pnd_id = pnd_id
# 1. Base Stats Precalc
jingak = "(진각)" in typ
base_stat_manual = None if jingak else _get_base_stats_cached(strip_jingak(typ))
bs = base_stat_manual if base_stat_manual else _get_base_stats_cached(strip_jingak(typ))
aw = _get_awakening_stat_cached(typ) if (jingak and not base_stat_manual) else {"hp":0,"atk":0,"def":0}
boost_h, boost_a, boost_d = grade_boost_for_type_precise_tuple(typ, grade)
self.c1 = (bs["hp"] + aw["hp"] + boost_h,
bs["atk"] + aw["atk"] + boost_a,
bs["def"] + aw["def"] + boost_d)
pct7, flat8, sub9 = _spirit_breakdown_cached(_sp_key_for_cache(sp["spec"]))
self.pct7 = (1+pct7["hp"], 1+pct7["atk"], 1+pct7["def"])
self.flat8 = (flat8["hp"], flat8["atk"], flat8["def"])
self.sub9 = (sub9["hp"], sub9["atk"], sub9["def"])
if pnd:
self.pnd_mul = (1.0+pnd.get("hp",0.0), 1.0+pnd.get("atk",0.0), 1.0+pnd.get("def",0.0))
else:
self.pnd_mul = (1.0, 1.0, 1.0)
self.coll_add = (_COLLECTION.get("hp",0), _COLLECTION.get("atk",0), _COLLECTION.get("def",0))
self.potion_add = (_POTION.get("hp",0), _POTION.get("atk",0), _POTION.get("def",0))
b = _pct_from(buff)
n = _pct_from(nerf)
self.eff = (b["hp"]-n["hp"], b["atk"]-n["atk"], b["def"]-n["def"])
self.bs_buff = (bs["hp"]+boost_h, bs["atk"]+boost_a, bs["def"]+boost_d)
self.acc_hp_pct = float((acc_row or {}).get("hp%",0))/100.0
self.acc_atk_pct = float((acc_row or {}).get("atk%",0))/100.0
self.acc_def_pct = float((acc_row or {}).get("def%",0))/100.0
# [핵심] 낙관적 현실주의: 내 인벤토리에서 '가장 좋은 젬'을 썼을 때의 점수
heur_ench = {"hp":0.21,"atk":0,"def":0} if (not no_enchant and (not allowed or "HP" in allowed)) else {"hp":0,"atk":0,"def":0}
b_score, _ = self.compute(optimistic_sums, heur_ench)
self.approx_bib = b_score
self.approx_tar = tar_percent(b_score, M_val)
def compute(self, gem_sums: Tuple[float,float,float], ench_pct: Dict[str,float]) -> Tuple[int, Dict[str,int]]:
# Inline Compute
# Base Total
base_h = self.c1[0] + gem_sums[0]*4 + self.potion_add[0]
base_a = self.c1[1] + gem_sums[1] + self.potion_add[1]
base_d = self.c1[2] + gem_sums[2] + self.potion_add[2]
# Mul1 (Acc + Ench)
m1_h = 1.0 + self.acc_hp_pct + ench_pct.get("hp",0.0)
m1_a = 1.0 + self.acc_atk_pct + ench_pct.get("atk",0.0)
m1_d = 1.0 + self.acc_def_pct + ench_pct.get("def",0.0)
st1_h = int(base_h * m1_h)
st1_a = int(base_a * m1_a)
st1_d = int(base_d * m1_d)
# Spirit (Pct7 + Flat8)
st2_h = int(st1_h * self.pct7[0] + self.flat8[0] * self.pct7[0])
st2_a = int(st1_a * self.pct7[1] + self.flat8[1] * self.pct7[1])
st2_d = int(st1_d * self.pct7[2] + self.flat8[2] * self.pct7[2])
# Pendant
st2_h = int(st2_h * self.pnd_mul[0])
st2_a = int(st2_a * self.pnd_mul[1])
st2_d = int(st2_d * self.pnd_mul[2])
# Collection + Sub9
st3_h = st2_h + self.coll_add[0] + self.sub9[0]
st3_a = st2_a + self.coll_add[1] + self.sub9[1]
st3_d = st2_d + self.coll_add[2] + self.sub9[2]
# Buff Add
add_h = int(self.bs_buff[0] * self.eff[0])
add_a = int(self.bs_buff[1] * self.eff[1])
add_d = int(self.bs_buff[2] * self.eff[2])
fh = st3_h + add_h
fa = st3_a + add_a
fd = st3_d + add_d
return (fh*fa*fd), {"hp":fh, "atk":fa, "def":fd}
def _assign_best_enchant_fast(cand: FastCandidate, sums: Tuple[float,float,float]) -> Tuple[str, Dict, int, Dict]:
if cand.no_enchant or not cand.allowed:
bib, final = cand.compute(sums, {"hp":0,"atk":0,"def":0})
return "없음", {"hp":0,"atk":0,"def":0}, bib, final
best_name = None
best_pct = None
best_bib = -1
best_final = None
# Try allowed enchants
for nm, pct in ENCH_LIST:
if cand.allowed and nm not in cand.allowed:
continue
bib, final = cand.compute(sums, pct)
if bib > best_bib:
best_bib = bib
best_name = nm
best_pct = pct
best_final = final
return best_name, best_pct, best_bib, best_final
# =============================================================================
# [수정됨] 길드전 최적화 제너레이터 (젬 패턴 중복 제거로 속도 15배 향상)
# =============================================================================
def guild_optimize_generator(
week_typ, week_buff,
slot_used_list, slot_typ_list, slot_buff_list, slot_grade_list, slot_nerf_list,
pendant_inv_rows,
acc_df, gem_df, spirits_rows,
top_k=120, min_tar_cut=0.0,
goal:str="비벨우선",
mode:str="중간"
) -> Generator[Tuple[pd.DataFrame, pd.DataFrame, dict], None, None]:
used_slots = [i+1 for i,u in enumerate(slot_used_list) if bool(u)]
if len(used_slots) < NUM_DECKS:
yield pd.DataFrame({"오류":[f"드래곤 슬롯 최소 {NUM_DECKS}개 사용 체크 필요"]}), pd.DataFrame({"평균 비벨":[0],"평균TAR":[0]}), {"choice":[]}
return
# 모드별 설정
if mode == "신속":
limit_base = 100
prune_factor = 0.90
depth_limits = [1.0, 0.3, 0.1]
elif mode == "전수":
limit_base = 50000
prune_factor = 1.0
depth_limits = [1.0, 1.0, 1.0]
else: # 중간
limit_base = 2000
prune_factor = 0.98
depth_limits = [1.0, 0.4, 0.2]
M = get_plain_M(2, week_typ, week_buff)
# ----- 장신구 풀 -----
try:
base = accmod.df_acc.copy()
base_key={f'{int(r.get("lv",19))} {str(r.get("이름","")).strip()}': r.to_dict() for _,r in base.iterrows()}
except Exception:
base_key={}
acc_pool=[]
if acc_df is not None and not acc_df.empty:
for _,r in acc_df.iterrows():
if not bool(r.get("사용",False)):
continue
tag = str(r.get("장신구","")).strip()
inst = str(r.get("인스턴스","#1")).strip() or "#1"
acc_row = base_key.get(tag)
if not acc_row:
continue
allowed = set()
if bool(r.get("HP",True)): allowed.add("HP")
if bool(r.get("ATK",True)): allowed.add("ATK")
if bool(r.get("DEF",True)): allowed.add("DEF")
acc_pool.append({
"key": f"{tag} {inst}",
"acc": acc_row,
"allowed": allowed,
"no_enchant": (not allowed)
})
if len(acc_pool) < NUM_DECKS:
yield pd.DataFrame({"오류":["장신구 인벤 사용 체크 필요"]}), pd.DataFrame({"평균 비벨":[0],"평균TAR":[0]}), {"choice":[]}
return
# ----- 젬 풀 -----
gem_pool_master = gem_inventory_to_pool(gem_df)
if gem_pool_master.total_count() < 5*NUM_DECKS:
yield pd.DataFrame({"오류":[f"젬 최소 {5*NUM_DECKS}개 필요"]}), pd.DataFrame({"평균 비벨":[0],"평균TAR":[0]}), {"choice":[]}
return
feasible_patterns = _feasible_dists_by_count(gem_pool_master)
if not feasible_patterns:
yield pd.DataFrame({"오류":["젬 분배 패턴 없음(인벤 확인)"]}), pd.DataFrame({"평균 비벨":[0],"평균TAR":[0]}), {"choice":[]}
# [최적화] 젬 패턴별 '낙관적 합계' 미리 계산 (반복 계산 제거)
precalc_gem_opts = []
for dist in feasible_patterns:
opt_sums = (
gem_pool_master.get_optimistic_sum(0, dist[0]),
gem_pool_master.get_optimistic_sum(1, dist[1]),
gem_pool_master.get_optimistic_sum(2, dist[2])
)
precalc_gem_opts.append((dist, opt_sums))
# ----- 정령 풀 -----
spirit_pool=[]
if spirits_rows:
for i,r in enumerate(spirits_rows, start=1):
if not r.get("사용",False):
continue
obj = make_spirit_obj(r)
bind = r.get("귀속","정령 귀속X")
bind_slot=None
if isinstance(bind,str) and bind.endswith("번드래곤"):
try: bind_slot=int(bind.replace("번드래곤",""))
except Exception: bind_slot=None
spirit_pool.append({"id":f"SP{i}","bind":bind_slot,"spec":obj})
if len(spirit_pool) < NUM_DECKS:
yield pd.DataFrame({"오류":[f"정령 최소 {NUM_DECKS}개 사용 체크 필요"]}), pd.DataFrame({"평균 비벨":[0],"평균TAR":[0]}), {"choice":[]}
return
# ----- 펜던트 풀 -----
pendant_pool=[]
for i,pr in enumerate(pendant_inv_rows or [], start=1):
if not pr.get("use",False):
continue
lines=[(pr.get("l1s"), pr.get("l1v",0)),
(pr.get("l2s"), pr.get("l2v",0)),
(pr.get("l3s"), pr.get("l3v",0))]
accp={"hp":0,"atk":0,"def":0}
for st,v in lines:
st2={"체력":"hp","공격력":"atk","방어력":"def"}.get(st)
v=max(0,min(6,int(v or 0)))
if st2: accp[st2]+=v
if accp["hp"]+accp["atk"]+accp["def"]>0:
pendant_pool.append({"id":f"PND{i}", "pct":{k:accp[k]/100.0 for k in accp}})
# 펜던트 미사용(None)도 후보에 포함
pendant_pool_with_none = [None] + pendant_pool
# ----- 후보 생성 (최적화됨) -----
cand_by_slot={}
for i in used_slots:
typ = slot_typ_list[i-1]
buf = slot_buff_list[i-1]
nerf = slot_nerf_list[i-1]
grade = slot_grade_list[i-1]
cands=[]
# [최적화 핵심] 루프 순서 변경: 장비 조합(Acc+Sp+Pnd)을 먼저 정하고, 그 조합에서 가장 좋은 젬 패턴 1개만 선택
for acc in acc_pool:
for sp in spirit_pool:
if sp.get("bind") not in (None,i):
continue
for pn in pendant_pool_with_none:
pnd_pct = None if pn is None else pn["pct"]
pnd_id = None if pn is None else pn["id"]
# 이 장비 조합에서 최고의 젬 패턴 찾기
best_fc = None
best_score = -1.0
for dist, opt_sums in precalc_gem_opts:
# 후보 객체 임시 생성 (빠름)
fc = FastCandidate(i, typ, buf, nerf, grade,
acc["key"], acc["acc"], acc["allowed"], acc["no_enchant"],
sp, dist, pnd_pct, pnd_id, M, opt_sums)
if fc.approx_bib > best_score:
best_score = fc.approx_bib
best_fc = fc
# 가장 좋은 젬 패턴을 가진 후보 1개만 등록
if best_fc:
cands.append(best_fc)
if not cands: continue
cands.sort(key=lambda x: x.approx_bib, reverse=True)
cand_by_slot[i] = cands
valid=[i for i in used_slots if cand_by_slot.get(i)]
if len(valid) < NUM_DECKS:
yield pd.DataFrame({"오류":["유효 후보 부족"]}), pd.DataFrame({"평균 비벨":[0],"평균TAR":[0]}), {"choice":[]}
return
slot_lists=[cand_by_slot[i] for i in valid]
# Pruning용 최대 점수 계산
max_scores = [cands[0].approx_bib for cands in slot_lists]
remaining_max = [0] * (NUM_DECKS + 1)
for k in range(NUM_DECKS - 1, -1, -1):
remaining_max[k] = max_scores[k] + remaining_max[k+1]
best_score = -1.0
best_choice = None
best_choice_bib_sum = 0
# 결과 포맷팅 헬퍼
def make_result(sel):
rows=[]
for i,c in enumerate(sel, start=1):
if c["used"] is None:
used_list = gem_pool_master.generate_used_gem_string(c["rollback"])
used_str = _compact_used_gems(used_list)
else:
used_str = c["used"]
rows.append({
"순번":i,"타입":c["typ"],"버프":c["buff"],"너프":c["nerf"], "등급":c["grade"],
"펜던트": pendant_label(c["pnd"]),
"젬분배":f"{c['dist'][0]}/{c['dist'][1]}/{c['dist'][2]}",
"젬":used_str,
"장신구":c["acc_key"].rsplit(" ",1)[0], "인첸트":c["ench"],
"정령": c["sp_label"],
"비벨":c["bib"], "TAR":round(c["T"],2),
"HP":c["final"]["hp"], "ATK":c["final"]["atk"], "DEF":c["final"]["def"]
})
df=pd.DataFrame(rows)
avg=pd.DataFrame({"평균 비벨":[int(sum(r["비벨"] for r in rows)/len(rows))],
"평균TAR":[round(sum(r["TAR"] for r in rows),2)/len(rows)]})
return df, avg, {"choice":sel}
# 재귀 탐색
def try_place(idx:int, sel:List[dict], pool: GemPool,
used_acc:Set[str], used_sp:Set[str], used_pn:Set[str], cur_bib_sum:int):
nonlocal best_score, best_choice, best_choice_bib_sum
if best_choice is not None:
if cur_bib_sum + remaining_max[idx] < best_choice_bib_sum * prune_factor:
return
if idx == NUM_DECKS:
sc = _score_tuple(sel, goal)
if (best_score == -1.0) or (sc > best_score):
best_score = sc
best_choice = list(sel)
best_choice_bib_sum = sum(c["bib"] for c in sel)
return
cur_slot_cands = slot_lists[idx]
count = 0
current_limit = max(limit_base * depth_limits[idx], int(top_k))
for c in cur_slot_cands:
if count >= current_limit: break
if c.acc_key in used_acc: continue
if c.sp["id"] in used_sp: continue
if c.pnd_id is not None and c.pnd_id in used_pn: continue
ok, sums, rollback_info = pool.allocate_fast(c.dist)
if not ok: continue
ench_name, _, best_bib, best_final = _assign_best_enchant_fast(c, sums)
T = tar_percent(best_bib, M)
if T < float(min_tar_cut):
pool.restore(rollback_info)
continue
sel.append({
"slot":c.slot, "typ":c.typ, "buff":c.buff, "nerf":c.nerf, "grade":c.grade,
"acc_key":c.acc_key, "ench":ench_name, "pnd":c.pnd, "pnd_id":c.pnd_id,
"dist":c.dist, "used":None, "final":best_final, "bib":best_bib, "T":T,
"sp_label": c.sp_label,
"rollback": rollback_info
})
used_acc.add(c.acc_key)
used_sp.add(c.sp["id"])
if c.pnd_id: used_pn.add(c.pnd_id)
yield from try_place(idx+1, sel, pool, used_acc, used_sp, used_pn, cur_bib_sum + best_bib)
sel.pop()
if c.pnd_id: used_pn.remove(c.pnd_id)
used_sp.remove(c.sp["id"])
used_acc.remove(c.acc_key)
pool.restore(rollback_info)
count += 1
gen = try_place(0, [], gem_pool_master.copy_fast(), set(), set(), set(), 0)
for _ in gen: pass
if not best_choice:
yield pd.DataFrame({"오류":["조합 없음"]}), pd.DataFrame({"평균 비벨":[0],"평균TAR":[0]}), {"choice":[]}
else:
df_res, df_avg, ctx_res = make_result(best_choice)
yield df_res, df_avg, ctx_res
def view_base_from_ctx(ctx):
choice = (ctx or {}).get("choice", [])
if not choice:
return pd.DataFrame({"오류": ["먼저 길드전 최적화를 실행하세요."]})
rows = []
for i, c in enumerate(choice, start=1):
final = c["final"]
typ = c["typ"]
grade = c["grade"]
base_stat = _get_base_stats_cached(strip_jingak(typ))
boost = grade_boost_for_type_precise(typ, grade)
bs_for_buff = {k: base_stat[k] + boost.get(k, 0) for k in ("hp", "atk", "def")}
buff = _pct_from(c["buff"])
nerf = _pct_from(c["nerf"])
inc = {k: _floor_int(bs_for_buff[k] * buff[k]) for k in ("hp","atk","def")}
dec = {k: _floor_int(bs_for_buff[k] * nerf[k]) for k in ("hp","atk","def")}
nobuff = {k: final[k] - inc[k] + dec[k] for k in ("hp","atk","def")}
rows.append({
"순번": i,
"타입": c["typ"],
"HP": nobuff["hp"],
"ATK": nobuff["atk"],
"DEF": nobuff["def"],
})
return pd.DataFrame(rows)
# =============================================================================
# UI
# =============================================================================
def build_ui():
with gr.Blocks(theme=gr.themes.Soft(), analytics_enabled=False, css=CSS) as demo:
gr.HTML("<h2>TAR 정령 자동 계산기 / 길드전 최적화</h2>")
visits_html = gr.HTML()
gr.HTML(FOCUS_KILL_JS)
with gr.Tabs():
# -------------------- 자동 계산기 --------------------
with gr.Tab("자동 계산기"):
with gr.Row():
# 좌1
with gr.Column(scale=1, min_width=260):
types = gr.CheckboxGroup(label="타입", choices=TYPES_ALL, value=BASE_TYPES)
buff = gr.Dropdown(label="버프", choices=["최적화","1벞 최적화","0벞"]+ALL_BUFF_CHOICES, value="최적화")
nerf = gr.Dropdown(label="너프", choices=ALL_BUFF_CHOICES, value="0벞")
grade_sel = gr.Dropdown(label="등급", choices=["7.0","8.0","9.0"], value="9.0")
gr.Markdown("### 펜던트(자동)")
pnd_opt_on = gr.Checkbox(label="펜던트 자동 최적화 사용", value=True)
pnd_total = gr.Slider(0,18, value=18, step=1, label="펜던트 총합(%)")
pnd_grade = gr.Dropdown(label="펜던트 등급", choices=["별","달","태양"], value="태양")
# 좌2
with gr.Column(scale=1, min_width=260):
# ✅ 디폴트 37만 체크
gemvals = gr.CheckboxGroup(label="젬 수치", choices=[str(v) for v in range(34,41)], value=["37"])
gr.Markdown("### 장신구")
acc_controls=[]
try:
base_df = accmod.df_acc.copy().sort_values(["lv","이름"])
except Exception:
base_df = pd.DataFrame([{"lv":19,"이름":"황보"}])
# [신규] 레벨별 전체 선택/해제 기능
levels = sorted(base_df["lv"].astype(int).unique())
for lv in levels:
with gr.Accordion(f"{lv} 레벨", open=(lv==19)):
with gr.Row():
# 버튼 생성
btn_all = gr.Button("전체 선택", size="sm")
btn_none = gr.Button("전체 해제", size="sm")
sub = base_df[base_df["lv"].astype(int)==lv]
current_lvl_cbs = []
for name in sub["이름"].astype(str).tolist():
label=f"{lv} {name}"
with gr.Row():
use=gr.Checkbox(label=f"{label} 사용", value=(lv==19))
with gr.Row():
hp = gr.Checkbox(label="HP", value=True)
atk= gr.Checkbox(label="ATK", value=True)
df = gr.Checkbox(label="DEF", value=True)
acc_controls.append((label,use,hp,atk,df))
current_lvl_cbs.append(use)
# 이벤트 연결
# 주의: Gradio에서 output list는 고정되어야 하므로 이 방식이 안전
cbs_count = len(current_lvl_cbs)
btn_all.click(fn=lambda n=cbs_count: [True]*n, outputs=current_lvl_cbs)
btn_none.click(fn=lambda n=cbs_count: [False]*n, outputs=current_lvl_cbs)
# 좌3
with gr.Column(scale=1, min_width=260):
stat_choices=["체력","공격력","방어력"]; mode_choices=["+","%"]
with gr.Row():
s1=gr.Dropdown(stat_choices, value="체력", label="1옵 스탯")
m1=gr.Dropdown(mode_choices, value="%", label="1옵 모드")
with gr.Row():
s2=gr.Dropdown(stat_choices, value="공격력", label="2옵 스탯")
m2=gr.Dropdown(mode_choices, value="%", label="2옵 모드")
with gr.Row():
s3=gr.Dropdown(stat_choices, value="방어력", label="3옵 스탯")
m3=gr.Dropdown(mode_choices, value="%", label="3옵 모드")
with gr.Row():
s4=gr.Dropdown(stat_choices, value="방어력", label="4옵 스탯")
m4=gr.Dropdown(mode_choices, value="+", label="4옵 모드")
subopt=gr.Dropdown(["체력40","공격력10","방어력10"], value="체력40", label="부가옵")
sp_skip=gr.Checkbox(label="정령 미사용", value=False)
# 좌4
with gr.Column(scale=1, min_width=260):
topn = gr.Slider(1, 3000, value=200, step=1, label="상위 N개")
sort_key = gr.Radio(["비벨","TAR%"], value="TAR%", label="정렬")
gr.Markdown("### 중복 제거")
dedup_type = gr.Checkbox(label="타입", value=False)
dedup_buff = gr.Checkbox(label="버프", value=False)
dedup_acc = gr.Checkbox(label="장신구", value=False)
dedup_ench = gr.Checkbox(label="인첸트", value=False)
dedup_sp = gr.Checkbox(label="정령", value=False)
# --- 펜던트(수동) ---
gr.Markdown("### 펜던트(수동)")
pnd_manual_on = gr.Checkbox(label="펜던트 수동 사용", value=False)
pnd_manual_entries=[]
for i in range(1,6+1):
with gr.Accordion(f"수동 펜던트 #{i}", open=False):
u = gr.Checkbox(label="사용", value=False)
with gr.Row():
l1s = gr.Dropdown(["체력","공격력","방어력"], value="체력", label="라인1 스탯")
l1v = gr.Slider(0,6,value=0,step=1,label="라인1 %")
with gr.Row():
l2s = gr.Dropdown(["체력","공격력","방어력"], value="공격력", label="라인2 스탯")
l2v = gr.Slider(0,6,value=0,step=1,label="라인2 %")
with gr.Row():
l3s = gr.Dropdown(["체력","공격력","방어력"], value="방어력", label="라인3 스탯")
l3v = gr.Slider(0,6,value=0,step=1,label="라인3 %")
pnd_manual_entries.append((u,l1s,l1v,l2s,l2v,l3s,l3v))
run_btn = gr.Button("계산하기", variant="primary")
reset_btn = gr.Button("리셋", variant="secondary")
table = gr.Dataframe(label="결과", interactive=False, wrap=True)
fav_msg = gr.HTML("")
fav_save_btn = gr.Button("즐겨찾기 저장")
fav_load_btn = gr.Button("즐겨찾기 불러오기")
auto_flat_inputs = [ctrl for g in acc_controls for ctrl in (g[1],g[2],g[3],g[4])]
pnd_manual_flat = [ctrl for e in pnd_manual_entries for ctrl in e]
def _collect_acc(*vals, acc_snapshot=acc_controls):
out=[]; it=iter(vals)
for (label,_u,_h,_a,_d) in acc_snapshot:
use=bool(next(it)); hp=bool(next(it)); atk=bool(next(it)); df=bool(next(it))
out.append((label,use,hp,atk,df))
return out
def _collect_pnd_manual(*vals, snapshot=pnd_manual_entries):
out=[]; it=iter(vals)
for _ in snapshot:
u=bool(next(it)); l1s=next(it); l1v=int(next(it) or 0); l2s=next(it); l2v=int(next(it) or 0); l3s=next(it); l3v=int(next(it) or 0)
out.append({"use":u,"l1s":l1s,"l1v":l1v,"l2s":l2s,"l2v":l2v,"l3s":l3s,"l3v":l3v})
return out
def _run_wrap(*vals, acc_snapshot=acc_controls, pnd_snapshot=pnd_manual_entries):
idx=0
types_sel = vals[idx]; idx+=1
gem_vals_sel = vals[idx]; idx+=1
acc_vals = vals[idx: idx+len(acc_snapshot)*4]; idx+=len(acc_snapshot)*4
acc_packed = _collect_acc(*acc_vals, acc_snapshot=acc_snapshot)
s1v,m1v,s2v,m2v,s3v,m3v,s4v,m4v,subv,spv = vals[idx:idx+10]; idx+=10
buff_mode = vals[idx]; idx+=1
nerf_mode = vals[idx]; idx+=1
grade_sel = vals[idx]; idx+=1
p_on, p_total, p_grade = vals[idx:idx+3]; idx+=3
pnd_man_on = vals[idx]; idx+=1
pnd_man_vals = vals[idx: idx+len(pnd_snapshot)*7]; idx+=len(pnd_snapshot)*7
pnd_manual = _collect_pnd_manual(*pnd_man_vals, snapshot=pnd_snapshot)
d_t,d_b,d_a,d_e,d_s = vals[idx:idx+5]; idx+=5
topnv,sortv = vals[idx:idx+2]; idx+=2
return auto_run(
types_sel, gem_vals_sel, acc_packed,
s1v,m1v,s2v,m2v,s3v,m3v,s4v,m4v,subv,spv,
buff_mode, nerf_mode, grade_sel,
False,
p_on, p_total, p_grade,
pnd_man_on, pnd_manual,
d_t,d_b,d_a,d_e,d_s,
topnv,sortv
)
run_btn.click(
fn=_run_wrap,
inputs=[types, gemvals] + auto_flat_inputs + [
s1,m1,s2,m2,s3,m3,s4,m4,subopt,sp_skip,
buff, nerf, grade_sel,
pnd_opt_on, pnd_total, pnd_grade,
pnd_manual_on] + pnd_manual_flat + [
dedup_type, dedup_buff, dedup_acc, dedup_ench, dedup_sp,
topn, sort_key
],
outputs=[table]
)
reset_btn.click(lambda: pd.DataFrame(), None, [table])
# 저장
def _auto_save(request: gr.Request|None, *vals):
try:
return fav_save_generic("auto", {"vals": list(vals)}, request)
except Exception as e:
return f"<span style='color:red;'>저장 실패: {e}</span>"
fav_save_btn.click(
fn=_auto_save,
inputs=[types, gemvals] + auto_flat_inputs + [
s1,m1,s2,m2,s3,m3,s4,m4,subopt,sp_skip,
buff, nerf, grade_sel,
pnd_opt_on, pnd_total, pnd_grade,
pnd_manual_on] + pnd_manual_flat + [
dedup_type, dedup_buff, dedup_acc, dedup_ench, dedup_sp,
topn, sort_key
],
outputs=[fav_msg]
)
# 불러오기
def _auto_load(request: gr.Request|None, acc_snapshot=acc_controls, pnd_snapshot=pnd_manual_entries):
data = fav_load_generic("auto", request)
vals = data.get("vals", [])
need = (2 + len(acc_snapshot)*4 + 10 + 3 + 3 + 1 + len(pnd_snapshot)*7 + 5 + 2)
if not vals:
return [gr.update()] * need + ["<span>저장본 없음</span>"]
vals = (vals + [gr.update()]*(need-len(vals)))[:need]
return vals + ["<span style='color:blue;'>불러옴</span>"]
fav_load_btn.click(
fn=_auto_load, inputs=[],
outputs=[types, gemvals] + auto_flat_inputs + [
s1,m1,s2,m2,s3,m3,s4,m4,subopt,sp_skip,
buff, nerf, grade_sel,
pnd_opt_on, pnd_total, pnd_grade,
pnd_manual_on] + pnd_manual_flat + [
dedup_type, dedup_buff, dedup_acc, dedup_ench, dedup_sp,
topn, sort_key, fav_msg
]
)
# -------------------- 길드전 셋팅 최적화 --------------------
with gr.Tab("길드전 셋팅 최적화"):
gr.Markdown("### 이번 주 기준")
with gr.Row():
week_typ = gr.Dropdown(label="타입", choices=BASE_TYPES, value="체")
week_buff= gr.Dropdown(label="버프(2벞)", choices=TWO_BUFFS, value="HP40%")
gr.Markdown("### 드래곤 슬롯")
slot_used=[]; slot_typ=[]; slot_buff=[]; slot_grade=[]; slot_nerf=[]
for i in range(1,7):
with gr.Accordion(f"드래곤 {i}", open=(i<=3)):
with gr.Row():
used = gr.Checkbox(label="사용", value=(i<=3)); slot_used.append(used)
s_type= gr.Dropdown(label="타입", choices=BASE_TYPES, value="체"); slot_typ.append(s_type)
s_buff= gr.Dropdown(label="버프", choices=ALL_BUFF_CHOICES, value="HP40%"); slot_buff.append(s_buff)
s_nerf= gr.Dropdown(label="너프", choices=ALL_BUFF_CHOICES, value="0벞"); slot_nerf.append(s_nerf)
s_grade=gr.Dropdown(label="등급", choices=["7.0","8.0","9.0"], value="7.0"); slot_grade.append(s_grade)
gr.Markdown("### 장신구 인벤")
acc_controls_g=[]
try:
base_df = accmod.df_acc.copy().sort_values(["lv","이름"])
except Exception:
base_df = pd.DataFrame([{"lv":19,"이름":"황보"}])
# [신규] 레벨별 전체 선택/해제 버튼 추가 (길드전 탭)
levels = sorted(base_df["lv"].astype(int).unique())
for lv in levels:
with gr.Accordion(f"{lv} 레벨", open=(lv==19)):
with gr.Row():
btn_all = gr.Button("전체 선택", size="sm")
btn_none = gr.Button("전체 해제", size="sm")
sub = base_df[base_df["lv"].astype(int)==lv]
lvl_cbs = [] # 이 레벨에 속한 모든 '사용' 체크박스들
for name in sub["이름"].astype(str).tolist():
label=f"{lv} {name}"
with gr.Accordion(f"{label}", open=False):
for inst in (1,2,3):
with gr.Row():
use= gr.Checkbox(label=f"{label} #{inst} 사용", value=False)
hp = gr.Checkbox(label="HP", value=True)
atk= gr.Checkbox(label="ATK", value=True)
df = gr.Checkbox(label="DEF", value=True)
acc_controls_g.append((label,inst,use,hp,atk,df))
lvl_cbs.append(use)
cbs_count = len(lvl_cbs)
btn_all.click(fn=lambda n=cbs_count: [True]*n, outputs=lvl_cbs)
btn_none.click(fn=lambda n=cbs_count: [False]*n, outputs=lvl_cbs)
def _collect_acc_df(*vals, acc_snapshot=acc_controls_g):
rows=[]; it=iter(vals)
for (label,inst,_u,_h,_a,_d) in acc_snapshot:
use=bool(next(it)); hp=bool(next(it)); atk=bool(next(it)); df=bool(next(it))
rows.append({"장신구":label,"인스턴스":f"#{inst}","사용":use,"HP":hp,"ATK":atk,"DEF":df})
return pd.DataFrame(rows)
gr.Markdown("### 펜던트 인벤 (1~3 기본 사용 체크)")
pend_controls=[]
# 펜던트 12개로 확장
for i in range(1,13):
with gr.Accordion(f"펜던트 #{i}", open=(i<=3)):
u=gr.Checkbox(label="사용", value=(i<=3))
with gr.Row():
l1s=gr.Dropdown(["체력","공격력","방어력"], value="체력", label="라인1 스탯"); l1v=gr.Slider(0,6,value=0,step=1,label="라인1 %")
with gr.Row():
l2s=gr.Dropdown(["체력","공격력","방어력"], value="공격력", label="라인2 스탯"); l2v=gr.Slider(0,6,value=0,step=1,label="라인2 %")
with gr.Row():
l3s=gr.Dropdown(["체력","공격력","방어력"], value="방어력", label="라인3 스탯"); l3v=gr.Slider(0,6,value=0,step=1,label="라인3 %")
pend_controls.append((u,l1s,l1v,l2s,l2v,l3s,l3v))
def _collect_pendant_inv(*vals, snapshot=pend_controls):
out=[]; it=iter(vals)
for _ in snapshot:
u=bool(next(it)); l1s=next(it); l1v=int(next(it) or 0); l2s=next(it); l2v=int(next(it) or 0); l3s=next(it); l3v=int(next(it) or 0)
out.append({"use":u,"l1s":l1s,"l1v":l1v,"l2s":l2s,"l2v":l2v,"l3s":l3s,"l3v":l3v})
return out
gr.Markdown("### 젬")
gem_df = gr.Dataframe(value=gem_inventory_default(), interactive=True, wrap=True)
gr.Markdown("### 정령")
spirits=[]
spirit_binds=["정령 귀속X"]+[f"{i}번드래곤" for i in range(1,7)]
# 정령 12개로 확장
for i in range(1,13):
d=spirit_default_row()
with gr.Accordion(f"정령 {i}", open=(i<=3)):
use=gr.Checkbox(label="사용", value=(i<=3))
bind=gr.Dropdown(label="정령 귀속", choices=spirit_binds, value="정령 귀속X")
s1_=gr.Dropdown(["체력","공격력","방어력"], value=d["1옵 스탯"], label="1옵 스탯")
m1_=gr.Dropdown(["+","%"], value=d["1옵 모드"], label="1옵 모드")
s2_=gr.Dropdown(["체력","공격력","방어력"], value=d["2옵 스탯"], label="2옵 스탯")
m2_=gr.Dropdown(["+","%"], value=d["2옵 모드"], label="2옵 모드")
s3_=gr.Dropdown(["체력","공격력","방어력"], value=d["3옵 스탯"], label="3옵 스탯")
m3_=gr.Dropdown(["+","%"], value=d["3옵 모드"], label="3옵 모드")
s4_=gr.Dropdown(["체력","공격력","방어력"], value=d["4옵 스탯"], label="4옵 스탯")
m4_=gr.Dropdown(["+","%"], value=d["4옵 모드"], label="4옵 모드")
sub_=gr.Dropdown(["체력40","공격력10","방어력10"], value=d["부가옵"], label="부가옵")
spirits.append((use,bind,s1_,m1_,s2_,m2_,s3_,m3_,s4_,m4_,sub_))
with gr.Row():
top_k = gr.Slider(1, 999, value=50, step=1, label="후보 상한(각 모드별 기본값에 곱연산)")
min_tar = gr.Slider(0, 999, value=0, step=1, label="TAR 하한")
# [신규] 계산 모드 선택 (실시간 삭제됨)
calc_mode = gr.Radio(["신속", "중간", "전수"], value="중간", label="계산 모드")
goal = gr.Radio(["비벨우선","TAR우선"], value="비벨우선", label="목표")
run2 = gr.Button("최적화 시작", variant="primary")
guild_table = gr.Dataframe(label="길드전 최적 세팅(3덱)", interactive=False, wrap=True, elem_id="guild_table_df")
guild_avg = gr.Dataframe(label="평균", interactive=False, wrap=True)
ctx_state = gr.State({})
flatten_acc_g=[]
for (_,_,use,hp,atk,df) in acc_controls_g:
flatten_acc_g.extend([use,hp,atk,df])
flatten_sp=[]
for s in spirits:
flatten_sp.extend(list(s))
flatten_pends=[]
for p in pend_controls:
flatten_pends.extend(list(p))
def _guild_run(week_typ, week_buff, *vals, acc_snapshot=acc_controls_g, pend_snapshot=pend_controls):
idx=0
used=[bool(vals[idx+i]) for i in range(6)]; idx+=6
styp=[vals[idx+i] for i in range(6)]; idx+=6
sbuf=[vals[idx+i] for i in range(6)]; idx+=6
sner=[vals[idx+i] for i in range(6)]; idx+=6
sgra=[vals[idx+i] for i in range(6)]; idx+=6
pend_vals = vals[idx: idx+len(pend_snapshot)*7]; idx+=len(pend_snapshot)*7
pnd_rows = _collect_pendant_inv(*pend_vals, snapshot=pend_snapshot)
gem_df_val=vals[idx]; idx+=1
acc_vals = vals[idx: idx+len(acc_snapshot)*4]; idx+=len(acc_snapshot)*4
acc_df_val = _collect_acc_df(*acc_vals, acc_snapshot=acc_snapshot)
spr_rows=[]
# 정령 12개 루프
for _ in range(12):
use=bool(vals[idx]); idx+=1
bind=vals[idx]; idx+=1
s1v=vals[idx]; idx+=1; m1v=vals[idx]; idx+=1
s2v=vals[idx]; idx+=1; m2v=vals[idx]; idx+=1
s3v=vals[idx]; idx+=1; m3v=vals[idx]; idx+=1
s4v=vals[idx]; idx+=1; m4v=vals[idx]; idx+=1
subv=vals[idx]; idx+=1
spr_rows.append({"사용":use,"귀속":bind,"1옵 스탯":s1v,"1옵 모드":m1v,"2옵 스탯":s2v,"2옵 모드":m2v,"3옵 스탯":s3v,"3옵 모드":m3v,"4옵 스탯":s4v,"4옵 모드":m4v,"부가옵":subv})
top=int(vals[idx]); idx+=1
mt=float(vals[idx]); idx+=1
mode_v=vals[idx]; idx+=1
goalv=vals[idx]; idx+=1
# 제너레이터 호출
gen = guild_optimize_generator(
week_typ, week_buff,
used, styp, sbuf, sgra, sner,
pnd_rows,
acc_df_val, gem_df_val, spr_rows,
top_k=top, min_tar_cut=mt,
goal=goalv,
mode=mode_v
)
# 제너레이터 소진 및 결과 반환
return next(gen)
run2.click(
fn=_guild_run,
inputs=[week_typ, week_buff] +
slot_used + slot_typ + slot_buff + slot_nerf + slot_grade +
flatten_pends + [gem_df] +
flatten_acc_g + flatten_sp +
[top_k, min_tar, calc_mode, goal],
outputs=[guild_table, guild_avg, ctx_state]
)
view_btn = gr.Button("버프 제외 스탯 보기(덱셋팅 스탯)", variant="secondary")
base_view = gr.Dataframe(label="기본 스탯", interactive=False, wrap=True)
view_btn.click(fn=view_base_from_ctx, inputs=[ctx_state], outputs=[base_view])
# 즐겨찾기 (길드전)
fav_msg_g = gr.HTML("")
fav_save_g = gr.Button("즐겨찾기 저장(길드전)")
fav_load_g = gr.Button("즐겨찾기 불러오기(길드전)")
def _fav_save_g(request: gr.Request|None, week_typ, week_buff, *vals, pend_snapshot=pend_controls):
try:
vals = list(vals)
gem_idx = 6*5 + len(pend_snapshot)*7
gem_df_val = vals[gem_idx]
if isinstance(gem_df_val, pd.DataFrame):
vals[gem_idx] = {"__gem_records__": gem_df_val.to_dict("records")}
payload = [week_typ, week_buff] + vals
return fav_save_generic("guild", {"vals": payload}, request)
except Exception as e:
return f"<span style='color:red;'>저장 실패: {e}</span>"
fav_save_g.click(
fn=_fav_save_g,
inputs=[week_typ, week_buff] +
slot_used + slot_typ + slot_buff + slot_nerf + slot_grade +
flatten_pends + [gem_df] +
flatten_acc_g + flatten_sp +
[top_k, min_tar, calc_mode, goal],
outputs=[fav_msg_g]
)
def _fav_load_g(request: gr.Request|None, acc_snapshot=acc_controls_g, pend_snapshot=pend_controls):
data = fav_load_generic("guild", request)
vals = data.get("vals", [])
if not vals:
# 정령 12개이므로 11개 속성 * 12
need = 2 + 6*5 + len(pend_snapshot)*7 + 1 + len(acc_snapshot)*4 + 12*11 + 4
return [gr.update()]*need + ["<span>저장본 없음</span>"]
vals = list(vals)
gem_pos = 2 + 6*5 + len(pend_snapshot)*7
if gem_pos < len(vals):
obj = vals[gem_pos]
if isinstance(obj, dict) and "__gem_records__" in obj:
vals[gem_pos] = pd.DataFrame(obj["__gem_records__"])
need = 2 + 6*5 + len(pend_snapshot)*7 + 1 + len(acc_snapshot)*4 + 12*11 + 4
vals = (vals + [gr.update()]*(need-len(vals)))[:need]
return vals + ["<span style='color:blue;'>불러옴</span>"]
fav_load_g.click(
fn=_fav_load_g, inputs=[],
outputs=[week_typ, week_buff] +
slot_used + slot_typ + slot_buff + slot_nerf + slot_grade +
flatten_pends + [gem_df] +
flatten_acc_g + flatten_sp +
[top_k, min_tar, calc_mode, goal, fav_msg_g]
)
# 방문자 카운트
def _visit(request: gr.Request):
return register_unique_visit(request)
demo.load(_visit, inputs=None, outputs=visits_html)
return demo
# =============================================================================
# Main
# =============================================================================
if __name__ == "__main__":
demo = build_ui()
demo.queue().launch(server_name="0.0.0.0", server_port=7860, show_api=False, share=False)