Add pet enhancer upgrade planning
Browse files- app.py +180 -1
- static/index.html +75 -10
- tests/test_market_optimizer_logic.py +29 -0
app.py
CHANGED
|
@@ -2123,6 +2123,9 @@ class UpgradeLabInput(BaseModel):
|
|
| 2123 |
strategy: str = "auto"
|
| 2124 |
elf_route: Optional[str] = None
|
| 2125 |
elf_level5_count: int = 0
|
|
|
|
|
|
|
|
|
|
| 2126 |
|
| 2127 |
|
| 2128 |
# ─────────────────────────────────────────────────────────────────
|
|
@@ -2874,6 +2877,94 @@ def _elf_result_distribution(level5_dist: dict, success_chance=0.8):
|
|
| 2874 |
result[successes] = result.get(successes, 0.0) + level5_prob * prob
|
| 2875 |
return dict(sorted(result.items()))
|
| 2876 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2877 |
_KNOWN_ELF_ROUTES = {
|
| 2878 |
("sword", "Dragon's Poison"): "Dragon's Devil",
|
| 2879 |
("shield", "Dragon's Anger"): "Dragon's Soul",
|
|
@@ -2955,6 +3046,15 @@ def _upgrade_lab_meta():
|
|
| 2955 |
"world": item.get("world", ""),
|
| 2956 |
"source": item.get("source", ""),
|
| 2957 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2958 |
return {
|
| 2959 |
"items": items,
|
| 2960 |
"swords": [
|
|
@@ -2965,6 +3065,8 @@ def _upgrade_lab_meta():
|
|
| 2965 |
{"name": item.get("name"), "rarity": item.get("rarity"), "world": item.get("world", "")}
|
| 2966 |
for item in (_GAME_CATALOG.get("shields") or [])
|
| 2967 |
],
|
|
|
|
|
|
|
| 2968 |
"elf_routes": _elf_routes(),
|
| 2969 |
"dwarf_luck": {
|
| 2970 |
rarity: [
|
|
@@ -2980,13 +3082,90 @@ def _upgrade_lab_meta():
|
|
| 2980 |
},
|
| 2981 |
}
|
| 2982 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2983 |
def _upgrade_lab_calculate(data: UpgradeLabInput):
|
| 2984 |
item_name = str(data.item_name or "").strip()
|
|
|
|
|
|
|
|
|
|
| 2985 |
singular, category, item = _upgrade_item_lookup(item_name, data.item_type)
|
| 2986 |
rarity = str(item.get("rarity") or "Common")
|
| 2987 |
target_level = max(1, min(10, int(data.target_level or 1)))
|
| 2988 |
target_count = max(1, int(data.target_count or 1))
|
| 2989 |
-
target_kind = str(data.target_kind or "level").strip().lower()
|
| 2990 |
strategy = str(data.strategy or "auto").strip().lower()
|
| 2991 |
if strategy not in ("auto", "dwarf", "dwarft", "dwarfting", "merge", "merging"):
|
| 2992 |
strategy = "auto"
|
|
|
|
| 2123 |
strategy: str = "auto"
|
| 2124 |
elf_route: Optional[str] = None
|
| 2125 |
elf_level5_count: int = 0
|
| 2126 |
+
player_level: Optional[int] = None
|
| 2127 |
+
pet_use_gold: bool = False
|
| 2128 |
+
pet_helper_odds: bool = False
|
| 2129 |
|
| 2130 |
|
| 2131 |
# ─────────────────────────────────────────────────────────────────
|
|
|
|
| 2877 |
result[successes] = result.get(successes, 0.0) + level5_prob * prob
|
| 2878 |
return dict(sorted(result.items()))
|
| 2879 |
|
| 2880 |
+
_PET_ENHANCER_ROUTES = [
|
| 2881 |
+
{"source": "Peppy", "result": "XPeppy"},
|
| 2882 |
+
{"source": "Blue Axolotl", "result": "XAxolotl"},
|
| 2883 |
+
{"source": "Pengoo", "result": "XPengoo"},
|
| 2884 |
+
{"source": "Leo", "result": "XLeo"},
|
| 2885 |
+
{"source": "Phoenix", "result": "XPhoenix"},
|
| 2886 |
+
{"source": "Octoo", "result": "XOctoo"},
|
| 2887 |
+
{"source": "Xen", "result": "XXen"},
|
| 2888 |
+
{"source": "Inferno", "result": "XInferno"},
|
| 2889 |
+
]
|
| 2890 |
+
|
| 2891 |
+
_PET_ENHANCER_CHANCES = [
|
| 2892 |
+
{"min": 0, "max": 2000, "free": [1.0, 0.5, 0.33, 0.25, 0.20, 0.17, 0.14, 0.13], "gold": [3.0, 1.5, 1.0, 0.75, 0.60, 0.50, 0.43, 0.38]},
|
| 2893 |
+
{"min": 2001, "max": 3000, "free": [3.0, 1.5, 1.0, 0.75, 0.60, 0.50, 0.43, 0.38], "gold": [9.0, 4.5, 3.0, 2.25, 1.80, 1.50, 1.29, 1.13]},
|
| 2894 |
+
{"min": 3001, "max": 4000, "free": [5.0, 2.5, 1.67, 1.25, 1.0, 0.83, 0.71, 0.63], "gold": [15.0, 7.5, 5.0, 3.75, 3.0, 2.50, 2.14, 1.88]},
|
| 2895 |
+
{"min": 4001, "max": 5000, "free": [7.0, 3.5, 2.33, 1.75, 1.40, 1.17, 1.0, 0.88], "gold": [21.0, 10.5, 7.0, 5.25, 4.20, 3.50, 3.0, 2.63]},
|
| 2896 |
+
{"min": 5001, "max": 6000, "free": [10.0, 5.0, 3.33, 2.50, 2.0, 1.67, 1.43, 1.25], "gold": [30.0, 15.0, 10.0, 7.50, 6.0, 5.0, 4.29, 3.75]},
|
| 2897 |
+
{"min": 6001, "max": 7000, "free": [15.0, 7.5, 5.0, 3.75, 3.0, 2.50, 2.14, 1.88], "gold": [45.0, 22.5, 15.0, 11.25, 9.0, 7.50, 6.43, 5.63]},
|
| 2898 |
+
{"min": 7001, "max": 8000, "free": [20.0, 10.0, 6.67, 5.0, 4.0, 3.33, 2.86, 2.50], "gold": [60.0, 30.0, 20.0, 15.0, 12.0, 10.0, 8.57, 7.50]},
|
| 2899 |
+
{"min": 8001, "max": 9000, "free": [25.0, 12.5, 8.33, 6.25, 5.0, 4.17, 3.57, 3.13], "gold": [75.0, 37.5, 25.0, 18.75, 15.0, 12.50, 10.71, 9.38]},
|
| 2900 |
+
{"min": 9001, "max": 10000, "free": [30.0, 15.0, 10.0, 7.50, 6.0, 5.0, 4.29, 3.75], "gold": [90.0, 45.0, 30.0, 22.50, 18.0, 15.0, 12.86, 11.25]},
|
| 2901 |
+
{"min": 10001, "max": None, "free": [33.0, 16.5, 11.0, 8.25, 6.60, 5.50, 4.71, 4.13], "gold": [99.0, 49.5, 33.0, 24.75, 19.80, 16.50, 14.14, 12.38]},
|
| 2902 |
+
]
|
| 2903 |
+
|
| 2904 |
+
def _pet_enhancer_routes():
|
| 2905 |
+
routes = []
|
| 2906 |
+
for index, route in enumerate(_PET_ENHANCER_ROUTES):
|
| 2907 |
+
source = route["source"]
|
| 2908 |
+
result = route["result"]
|
| 2909 |
+
if source not in PETS or result not in PETS:
|
| 2910 |
+
continue
|
| 2911 |
+
routes.append({
|
| 2912 |
+
"key": f"pet|{source}|{result}",
|
| 2913 |
+
"item_type": "pet",
|
| 2914 |
+
"source": source,
|
| 2915 |
+
"result": result,
|
| 2916 |
+
"index": index,
|
| 2917 |
+
})
|
| 2918 |
+
return routes
|
| 2919 |
+
|
| 2920 |
+
def _pet_enhancer_route_for_name(name: str):
|
| 2921 |
+
clean = str(name or "").strip().lower()
|
| 2922 |
+
for route in _pet_enhancer_routes():
|
| 2923 |
+
if route["source"].lower() == clean or route["result"].lower() == clean:
|
| 2924 |
+
return route
|
| 2925 |
+
return None
|
| 2926 |
+
|
| 2927 |
+
def _pet_enhancer_tier(player_level: int, helper_odds: bool = False):
|
| 2928 |
+
if helper_odds:
|
| 2929 |
+
return _PET_ENHANCER_CHANCES[-1]
|
| 2930 |
+
try:
|
| 2931 |
+
level = max(0, int(player_level or 0))
|
| 2932 |
+
except (TypeError, ValueError):
|
| 2933 |
+
level = 0
|
| 2934 |
+
for row in _PET_ENHANCER_CHANCES:
|
| 2935 |
+
if level >= row["min"] and (row["max"] is None or level <= row["max"]):
|
| 2936 |
+
return row
|
| 2937 |
+
return _PET_ENHANCER_CHANCES[-1]
|
| 2938 |
+
|
| 2939 |
+
def _pet_enhancer_chance(route: dict, player_level: int, use_gold: bool, helper_odds: bool):
|
| 2940 |
+
tier = _pet_enhancer_tier(player_level, helper_odds)
|
| 2941 |
+
key = "gold" if use_gold else "free"
|
| 2942 |
+
index = int(route.get("index", 0) or 0)
|
| 2943 |
+
chance_pct = float(tier[key][index])
|
| 2944 |
+
return max(0.0, min(1.0, chance_pct / 100.0)), chance_pct, tier
|
| 2945 |
+
|
| 2946 |
+
def _binomial_distribution(n: int, p: float):
|
| 2947 |
+
n = max(0, int(n or 0))
|
| 2948 |
+
p = max(0.0, min(1.0, float(p or 0.0)))
|
| 2949 |
+
if n <= 0:
|
| 2950 |
+
return {0: 1.0}
|
| 2951 |
+
if p <= 0.0:
|
| 2952 |
+
return {0: 1.0}
|
| 2953 |
+
if p >= 1.0:
|
| 2954 |
+
return {n: 1.0}
|
| 2955 |
+
dist = {}
|
| 2956 |
+
for successes in range(n + 1):
|
| 2957 |
+
prob = math.exp(
|
| 2958 |
+
math.lgamma(n + 1)
|
| 2959 |
+
- math.lgamma(successes + 1)
|
| 2960 |
+
- math.lgamma(n - successes + 1)
|
| 2961 |
+
+ successes * math.log(p)
|
| 2962 |
+
+ (n - successes) * math.log1p(-p)
|
| 2963 |
+
)
|
| 2964 |
+
if prob > 1e-15:
|
| 2965 |
+
dist[successes] = prob
|
| 2966 |
+
return dist
|
| 2967 |
+
|
| 2968 |
_KNOWN_ELF_ROUTES = {
|
| 2969 |
("sword", "Dragon's Poison"): "Dragon's Devil",
|
| 2970 |
("shield", "Dragon's Anger"): "Dragon's Soul",
|
|
|
|
| 3046 |
"world": item.get("world", ""),
|
| 3047 |
"source": item.get("source", ""),
|
| 3048 |
})
|
| 3049 |
+
for route in _pet_enhancer_routes():
|
| 3050 |
+
items.append({
|
| 3051 |
+
"name": route["source"],
|
| 3052 |
+
"item_type": "pet",
|
| 3053 |
+
"rarity": "Pet",
|
| 3054 |
+
"world": "Enhancer",
|
| 3055 |
+
"source": "Pet Enhancer",
|
| 3056 |
+
"enhances_to": route["result"],
|
| 3057 |
+
})
|
| 3058 |
return {
|
| 3059 |
"items": items,
|
| 3060 |
"swords": [
|
|
|
|
| 3065 |
{"name": item.get("name"), "rarity": item.get("rarity"), "world": item.get("world", "")}
|
| 3066 |
for item in (_GAME_CATALOG.get("shields") or [])
|
| 3067 |
],
|
| 3068 |
+
"pet_routes": _pet_enhancer_routes(),
|
| 3069 |
+
"pet_chance_tiers": _PET_ENHANCER_CHANCES,
|
| 3070 |
"elf_routes": _elf_routes(),
|
| 3071 |
"dwarf_luck": {
|
| 3072 |
rarity: [
|
|
|
|
| 3082 |
},
|
| 3083 |
}
|
| 3084 |
|
| 3085 |
+
def _upgrade_lab_pet_calculate(data: UpgradeLabInput):
|
| 3086 |
+
item_name = str(data.item_name or "").strip()
|
| 3087 |
+
route = _pet_enhancer_route_for_name(item_name)
|
| 3088 |
+
if not route:
|
| 3089 |
+
raise HTTPException(400, f"{item_name!r} has no known Pet Enhancer route.")
|
| 3090 |
+
target_count = max(1, int(data.target_count or 1))
|
| 3091 |
+
counts = data.counts or {}
|
| 3092 |
+
normal_count = max(0, int(counts.get("pet", counts.get("1", counts.get(1, 0))) or 0))
|
| 3093 |
+
attempts = normal_count // 10
|
| 3094 |
+
leftovers = normal_count % 10
|
| 3095 |
+
player_level = 10001 if data.pet_helper_odds else max(0, int(data.player_level or 0))
|
| 3096 |
+
chance, chance_pct, tier = _pet_enhancer_chance(route, player_level, data.pet_use_gold, data.pet_helper_odds)
|
| 3097 |
+
result_dist = _binomial_distribution(attempts, chance)
|
| 3098 |
+
probability = _distribution_tail(result_dist, target_count)
|
| 3099 |
+
expected = attempts * chance
|
| 3100 |
+
gold_cost = attempts * 1e15 if data.pet_use_gold else 0.0
|
| 3101 |
+
return {
|
| 3102 |
+
"ok": True,
|
| 3103 |
+
"item": {
|
| 3104 |
+
"type": "pet",
|
| 3105 |
+
"name": route["source"],
|
| 3106 |
+
"rarity": "Pet",
|
| 3107 |
+
"world": "Enhancer",
|
| 3108 |
+
"source": "Pet Enhancer",
|
| 3109 |
+
},
|
| 3110 |
+
"prices": {},
|
| 3111 |
+
"goal": {
|
| 3112 |
+
"kind": "pet",
|
| 3113 |
+
"label": route["result"],
|
| 3114 |
+
"target_count": target_count,
|
| 3115 |
+
"source_name": route["source"],
|
| 3116 |
+
"result_name": route["result"],
|
| 3117 |
+
},
|
| 3118 |
+
"plan": {
|
| 3119 |
+
"strategy": "pet_enhancer",
|
| 3120 |
+
"probability": probability,
|
| 3121 |
+
"probability_pct": probability * 100.0,
|
| 3122 |
+
"expected_count": expected,
|
| 3123 |
+
"distribution": _distribution_rows(result_dist),
|
| 3124 |
+
"steps": [{
|
| 3125 |
+
"from_level": 0,
|
| 3126 |
+
"to_level": 0,
|
| 3127 |
+
"action": "enhance",
|
| 3128 |
+
"chance": chance,
|
| 3129 |
+
"chance_pct": chance_pct,
|
| 3130 |
+
"expected_input": float(normal_count),
|
| 3131 |
+
"expected_output": expected,
|
| 3132 |
+
"merge_required": 10,
|
| 3133 |
+
"from_label": f"10x {route['source']}",
|
| 3134 |
+
"to_label": route["result"],
|
| 3135 |
+
"attempts": attempts,
|
| 3136 |
+
"leftovers": leftovers,
|
| 3137 |
+
"gold_cost": gold_cost,
|
| 3138 |
+
"gold_cost_display": _price_fmt(gold_cost) if gold_cost else "0",
|
| 3139 |
+
}],
|
| 3140 |
+
},
|
| 3141 |
+
"valuation": {
|
| 3142 |
+
"gold_cost": gold_cost,
|
| 3143 |
+
"gold_cost_display": _price_fmt(gold_cost) if gold_cost else "0",
|
| 3144 |
+
},
|
| 3145 |
+
"pet": {
|
| 3146 |
+
"route": route,
|
| 3147 |
+
"source_count": normal_count,
|
| 3148 |
+
"attempts": attempts,
|
| 3149 |
+
"leftovers": leftovers,
|
| 3150 |
+
"player_level": player_level,
|
| 3151 |
+
"helper_odds": bool(data.pet_helper_odds),
|
| 3152 |
+
"use_gold": bool(data.pet_use_gold),
|
| 3153 |
+
"chance": chance,
|
| 3154 |
+
"chance_pct": chance_pct,
|
| 3155 |
+
"tier": tier,
|
| 3156 |
+
},
|
| 3157 |
+
"elf": {"eligible": False, "routes": _elf_routes(), "route": None},
|
| 3158 |
+
}
|
| 3159 |
+
|
| 3160 |
def _upgrade_lab_calculate(data: UpgradeLabInput):
|
| 3161 |
item_name = str(data.item_name or "").strip()
|
| 3162 |
+
target_kind = str(data.target_kind or "level").strip().lower()
|
| 3163 |
+
if target_kind in ("pet", "xpet", "pet_enhancer", "enhancer"):
|
| 3164 |
+
return _upgrade_lab_pet_calculate(data)
|
| 3165 |
singular, category, item = _upgrade_item_lookup(item_name, data.item_type)
|
| 3166 |
rarity = str(item.get("rarity") or "Common")
|
| 3167 |
target_level = max(1, min(10, int(data.target_level or 1)))
|
| 3168 |
target_count = max(1, int(data.target_count or 1))
|
|
|
|
| 3169 |
strategy = str(data.strategy or "auto").strip().lower()
|
| 3170 |
if strategy not in ("auto", "dwarf", "dwarft", "dwarfting", "merge", "merging"):
|
| 3171 |
strategy = "auto"
|
static/index.html
CHANGED
|
@@ -716,13 +716,26 @@ input::placeholder{color:var(--text3);}
|
|
| 716 |
<select id="up-target-kind" onchange="upgradeGoalChanged()">
|
| 717 |
<option value="level">Item Level</option>
|
| 718 |
<option value="elf">Elf Legendary</option>
|
|
|
|
| 719 |
</select>
|
| 720 |
</div>
|
| 721 |
<div id="up-target-level-wrap"><label>Target Level</label><input id="up-target-level" type="number" value="5" min="1" max="10"></div>
|
| 722 |
<div><label>Target Count</label><input id="up-target-count" type="number" value="1" min="1"></div>
|
| 723 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 724 |
<div id="up-elf-hint" class="upgrade-mini" style="display:none;margin-top:6px"></div>
|
| 725 |
-
<div class="stitle">Items Owned By Level</div>
|
| 726 |
<div id="up-count-grid" class="upgrade-count-grid"></div>
|
| 727 |
<button class="btn btn-p" style="width:100%;justify-content:center;margin-top:12px;padding:9px" onclick="upgradeCalc()">CALCULATE</button>
|
| 728 |
<div id="up-status" class="upgrade-mini" style="margin-top:9px;min-height:14px">Choose an item, enter counts, or import an account.</div>
|
|
@@ -1626,26 +1639,59 @@ function _upgradeRouteForItem(name){
|
|
| 1626 |
return (UPGRADE_META?.elf_routes||[]).find(row=>String(row.source||'').toLowerCase()===clean)||null;
|
| 1627 |
}
|
| 1628 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1629 |
function upgradeItemChanged(syncRoute=true){
|
| 1630 |
const name=document.getElementById('up-item-name')?.value||'';
|
| 1631 |
const route=_upgradeRouteForItem(name);
|
|
|
|
| 1632 |
const goal=document.getElementById('up-target-kind');
|
| 1633 |
if(goal&&goal.value==='elf'&&!route)goal.value='level';
|
|
|
|
|
|
|
| 1634 |
upgradeGoalChanged();
|
| 1635 |
}
|
| 1636 |
|
| 1637 |
function upgradeGoalChanged(){
|
| 1638 |
const goal=document.getElementById('up-target-kind');
|
| 1639 |
-
const
|
|
|
|
|
|
|
| 1640 |
const elfOption=goal?.querySelector('option[value="elf"]');
|
| 1641 |
if(elfOption)elfOption.disabled=!route;
|
|
|
|
|
|
|
| 1642 |
if(goal&&goal.value==='elf'&&!route)goal.value='level';
|
|
|
|
|
|
|
| 1643 |
const kind=goal?.value||'level';
|
| 1644 |
const wrap=document.getElementById('up-target-level-wrap');
|
| 1645 |
-
if(wrap)wrap.style.display=kind==='elf'?'none':'';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1646 |
const hint=document.getElementById('up-elf-hint');
|
| 1647 |
if(hint){
|
| 1648 |
-
if(kind==='
|
|
|
|
|
|
|
|
|
|
| 1649 |
hint.style.display='';
|
| 1650 |
hint.textContent=`Elf: 10x ${route.source} Lv5 -> ${route.result}. Success chance is 80%; success and fail both consume the 10 Epics.`;
|
| 1651 |
}else if(route){
|
|
@@ -1677,6 +1723,13 @@ function _upgradeLoadedCountsFor(name){
|
|
| 1677 |
const clean=String(name||'').trim().toLowerCase();
|
| 1678 |
const counts={};
|
| 1679 |
for(let level=1;level<=10;level++)counts[String(level)]=0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1680 |
for(const row of UPGRADE_LOADED_ITEMS||[]){
|
| 1681 |
if(String(row.name||'').trim().toLowerCase()!==clean)continue;
|
| 1682 |
const level=Math.max(1,Math.min(10,parseInt(row.level,10)||1));
|
|
@@ -1729,8 +1782,12 @@ async function upgradeLoadAccount(){
|
|
| 1729 |
const d=bulk.results?.[name]||Object.values(bulk.results||{})[0];
|
| 1730 |
if(!d||!d.ok)throw new Error(d?.error||'No live lookup result.');
|
| 1731 |
const inv=d.inventory||{};
|
| 1732 |
-
UPGRADE_LOADED_ITEMS=[
|
| 1733 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1734 |
const input=document.getElementById('up-item-name');
|
| 1735 |
if(input&&!input.value.trim()){
|
| 1736 |
const best=_upgradeBestLoadedItem();
|
|
@@ -1743,8 +1800,8 @@ async function upgradeLoadAccount(){
|
|
| 1743 |
if(status){
|
| 1744 |
status.style.color=total>0?'var(--green)':'var(--gold)';
|
| 1745 |
status.textContent=total>0
|
| 1746 |
-
? `Loaded ${UPGRADE_LOADED_ITEMS.length}
|
| 1747 |
-
: `Loaded ${UPGRADE_LOADED_ITEMS.length}
|
| 1748 |
}
|
| 1749 |
}catch(e){
|
| 1750 |
if(status){status.style.color='var(--red)';status.textContent=`Import failed: ${e.message}`;}
|
|
@@ -1798,7 +1855,10 @@ async function upgradeCalc(){
|
|
| 1798 |
const itemName=document.getElementById('up-item-name')?.value||'';
|
| 1799 |
const meta=_upgradeItemMeta(itemName);
|
| 1800 |
const route=_upgradeRouteForItem(itemName);
|
| 1801 |
-
const
|
|
|
|
|
|
|
|
|
|
| 1802 |
const payload={
|
| 1803 |
item_type:meta?.item_type||'',
|
| 1804 |
item_name:itemName,
|
|
@@ -1808,6 +1868,9 @@ async function upgradeCalc(){
|
|
| 1808 |
target_level:parseInt(document.getElementById('up-target-level')?.value,10)||1,
|
| 1809 |
target_count:parseInt(document.getElementById('up-target-count')?.value,10)||1,
|
| 1810 |
elf_route:kind==='elf'&&route?route.key:null,
|
|
|
|
|
|
|
|
|
|
| 1811 |
};
|
| 1812 |
try{
|
| 1813 |
const data=await post('/upgrade-lab',payload);
|
|
@@ -1835,12 +1898,14 @@ function renderUpgradeResult(data){
|
|
| 1835 |
? `${row.merge_required}x required`
|
| 1836 |
: row.action==='merge+dwarf'
|
| 1837 |
? `${row.merge_required}x merge, leftovers dwarf ${Number(row.dwarf_chance_pct||0).toFixed(2)}%`
|
|
|
|
|
|
|
| 1838 |
: row.action==='elf'
|
| 1839 |
? '10x Lv5, 80%, fail destroys inputs'
|
| 1840 |
: `${Number(row.chance_pct||0).toFixed(2)}% success`;
|
| 1841 |
return `<tr>
|
| 1842 |
<td>${_escHtml(from)} -> ${_escHtml(to)}</td>
|
| 1843 |
-
<td style="color:${row.action==='merge'?'var(--green)':row.action==='merge+dwarf'?'var(--gold)':row.action==='elf'?'var(--gold)':'var(--accent)'}">${_escHtml(action)}</td>
|
| 1844 |
<td>${_escHtml(rule)}</td>
|
| 1845 |
<td>${_numText(row.expected_input,3)}</td>
|
| 1846 |
<td>${_numText(row.expected_output,3)}</td>
|
|
|
|
| 716 |
<select id="up-target-kind" onchange="upgradeGoalChanged()">
|
| 717 |
<option value="level">Item Level</option>
|
| 718 |
<option value="elf">Elf Legendary</option>
|
| 719 |
+
<option value="pet">Pet Enhancer</option>
|
| 720 |
</select>
|
| 721 |
</div>
|
| 722 |
<div id="up-target-level-wrap"><label>Target Level</label><input id="up-target-level" type="number" value="5" min="1" max="10"></div>
|
| 723 |
<div><label>Target Count</label><input id="up-target-count" type="number" value="1" min="1"></div>
|
| 724 |
</div>
|
| 725 |
+
<div id="up-pet-controls" class="fr c3" style="display:none;margin-top:7px">
|
| 726 |
+
<div><label>Player Level</label><input id="up-player-level" type="number" value="10000" min="0"></div>
|
| 727 |
+
<div><label>Odds</label>
|
| 728 |
+
<select id="up-pet-cost">
|
| 729 |
+
<option value="free">Free</option>
|
| 730 |
+
<option value="gold">With 1QT Gold</option>
|
| 731 |
+
</select>
|
| 732 |
+
</div>
|
| 733 |
+
<label style="display:flex;align-items:center;gap:6px;margin-top:17px;color:var(--text2);font-family:'Rajdhani',sans-serif;font-size:11px;letter-spacing:1px">
|
| 734 |
+
<input id="up-helper-odds" type="checkbox" style="width:auto"> 10K+ HELPER ODDS
|
| 735 |
+
</label>
|
| 736 |
+
</div>
|
| 737 |
<div id="up-elf-hint" class="upgrade-mini" style="display:none;margin-top:6px"></div>
|
| 738 |
+
<div class="stitle" id="up-count-title">Items Owned By Level</div>
|
| 739 |
<div id="up-count-grid" class="upgrade-count-grid"></div>
|
| 740 |
<button class="btn btn-p" style="width:100%;justify-content:center;margin-top:12px;padding:9px" onclick="upgradeCalc()">CALCULATE</button>
|
| 741 |
<div id="up-status" class="upgrade-mini" style="margin-top:9px;min-height:14px">Choose an item, enter counts, or import an account.</div>
|
|
|
|
| 1639 |
return (UPGRADE_META?.elf_routes||[]).find(row=>String(row.source||'').toLowerCase()===clean)||null;
|
| 1640 |
}
|
| 1641 |
|
| 1642 |
+
function _upgradePetRouteForItem(name){
|
| 1643 |
+
const clean=String(name||'').trim().toLowerCase();
|
| 1644 |
+
return (UPGRADE_META?.pet_routes||[]).find(row=>
|
| 1645 |
+
String(row.source||'').toLowerCase()===clean || String(row.result||'').toLowerCase()===clean
|
| 1646 |
+
)||null;
|
| 1647 |
+
}
|
| 1648 |
+
|
| 1649 |
function upgradeItemChanged(syncRoute=true){
|
| 1650 |
const name=document.getElementById('up-item-name')?.value||'';
|
| 1651 |
const route=_upgradeRouteForItem(name);
|
| 1652 |
+
const petRoute=_upgradePetRouteForItem(name);
|
| 1653 |
const goal=document.getElementById('up-target-kind');
|
| 1654 |
if(goal&&goal.value==='elf'&&!route)goal.value='level';
|
| 1655 |
+
if(goal&&goal.value==='pet'&&!petRoute)goal.value='level';
|
| 1656 |
+
if(goal&&goal.value==='level'&&petRoute)goal.value='pet';
|
| 1657 |
upgradeGoalChanged();
|
| 1658 |
}
|
| 1659 |
|
| 1660 |
function upgradeGoalChanged(){
|
| 1661 |
const goal=document.getElementById('up-target-kind');
|
| 1662 |
+
const itemName=document.getElementById('up-item-name')?.value||'';
|
| 1663 |
+
const route=_upgradeRouteForItem(itemName);
|
| 1664 |
+
const petRoute=_upgradePetRouteForItem(itemName);
|
| 1665 |
const elfOption=goal?.querySelector('option[value="elf"]');
|
| 1666 |
if(elfOption)elfOption.disabled=!route;
|
| 1667 |
+
const petOption=goal?.querySelector('option[value="pet"]');
|
| 1668 |
+
if(petOption)petOption.disabled=!petRoute;
|
| 1669 |
if(goal&&goal.value==='elf'&&!route)goal.value='level';
|
| 1670 |
+
if(goal&&goal.value==='pet'&&!petRoute)goal.value='level';
|
| 1671 |
+
if(goal&&goal.value==='level'&&petRoute)goal.value='pet';
|
| 1672 |
const kind=goal?.value||'level';
|
| 1673 |
const wrap=document.getElementById('up-target-level-wrap');
|
| 1674 |
+
if(wrap)wrap.style.display=(kind==='elf'||kind==='pet')?'none':'';
|
| 1675 |
+
const petControls=document.getElementById('up-pet-controls');
|
| 1676 |
+
if(petControls)petControls.style.display=kind==='pet'?'grid':'none';
|
| 1677 |
+
const countTitle=document.getElementById('up-count-title');
|
| 1678 |
+
if(countTitle)countTitle.textContent=kind==='pet'?'Normal Pets Owned':'Items Owned By Level';
|
| 1679 |
+
document.querySelectorAll('#up-count-grid>div').forEach((cell,index)=>{
|
| 1680 |
+
const label=cell.querySelector('label');
|
| 1681 |
+
if(kind==='pet'){
|
| 1682 |
+
cell.style.display=index===0?'':'none';
|
| 1683 |
+
if(label)label.textContent=petRoute?petRoute.source:'Normal Pet';
|
| 1684 |
+
}else{
|
| 1685 |
+
cell.style.display='';
|
| 1686 |
+
if(label)label.textContent=`Lv${index+1}`;
|
| 1687 |
+
}
|
| 1688 |
+
});
|
| 1689 |
const hint=document.getElementById('up-elf-hint');
|
| 1690 |
if(hint){
|
| 1691 |
+
if(kind==='pet'&&petRoute){
|
| 1692 |
+
hint.style.display='';
|
| 1693 |
+
hint.textContent=`Pet Enhancer: 10x ${petRoute.source} -> ${petRoute.result}. Failed attempts consume the 10 pets too.`;
|
| 1694 |
+
}else if(kind==='elf'&&route){
|
| 1695 |
hint.style.display='';
|
| 1696 |
hint.textContent=`Elf: 10x ${route.source} Lv5 -> ${route.result}. Success chance is 80%; success and fail both consume the 10 Epics.`;
|
| 1697 |
}else if(route){
|
|
|
|
| 1723 |
const clean=String(name||'').trim().toLowerCase();
|
| 1724 |
const counts={};
|
| 1725 |
for(let level=1;level<=10;level++)counts[String(level)]=0;
|
| 1726 |
+
const petRoute=_upgradePetRouteForItem(name);
|
| 1727 |
+
if(petRoute){
|
| 1728 |
+
counts['1']=(UPGRADE_LOADED_ITEMS||[]).filter(row=>
|
| 1729 |
+
row.item_type==='pet' && String(row.name||'').trim().toLowerCase()===String(petRoute.source||'').toLowerCase()
|
| 1730 |
+
).length;
|
| 1731 |
+
return counts;
|
| 1732 |
+
}
|
| 1733 |
for(const row of UPGRADE_LOADED_ITEMS||[]){
|
| 1734 |
if(String(row.name||'').trim().toLowerCase()!==clean)continue;
|
| 1735 |
const level=Math.max(1,Math.min(10,parseInt(row.level,10)||1));
|
|
|
|
| 1782 |
const d=bulk.results?.[name]||Object.values(bulk.results||{})[0];
|
| 1783 |
if(!d||!d.ok)throw new Error(d?.error||'No live lookup result.');
|
| 1784 |
const inv=d.inventory||{};
|
| 1785 |
+
UPGRADE_LOADED_ITEMS=[
|
| 1786 |
+
...(inv.swords||[]).map(row=>({...row,item_type:'sword'})),
|
| 1787 |
+
...(inv.shields||[]).map(row=>({...row,item_type:'shield'})),
|
| 1788 |
+
...(inv.pets||[]).map(row=>({...row,item_type:'pet'})),
|
| 1789 |
+
].filter(row=>row&&row.name);
|
| 1790 |
+
if(!UPGRADE_LOADED_ITEMS.length)throw new Error('No swords, shields or pets found in this live inventory.');
|
| 1791 |
const input=document.getElementById('up-item-name');
|
| 1792 |
if(input&&!input.value.trim()){
|
| 1793 |
const best=_upgradeBestLoadedItem();
|
|
|
|
| 1800 |
if(status){
|
| 1801 |
status.style.color=total>0?'var(--green)':'var(--gold)';
|
| 1802 |
status.textContent=total>0
|
| 1803 |
+
? `Loaded ${UPGRADE_LOADED_ITEMS.length} items from ${d.username||name}. Showing ${total}x ${input?.value||'selected item'}.`
|
| 1804 |
+
: `Loaded ${UPGRADE_LOADED_ITEMS.length} items from ${d.username||name}. No ${input?.value||'selected item'} found.`;
|
| 1805 |
}
|
| 1806 |
}catch(e){
|
| 1807 |
if(status){status.style.color='var(--red)';status.textContent=`Import failed: ${e.message}`;}
|
|
|
|
| 1855 |
const itemName=document.getElementById('up-item-name')?.value||'';
|
| 1856 |
const meta=_upgradeItemMeta(itemName);
|
| 1857 |
const route=_upgradeRouteForItem(itemName);
|
| 1858 |
+
const petRoute=_upgradePetRouteForItem(itemName);
|
| 1859 |
+
let kind=document.getElementById('up-target-kind')?.value||'level';
|
| 1860 |
+
if(petRoute&&kind==='level')kind='pet';
|
| 1861 |
+
if(kind==='pet'&&!petRoute)kind='level';
|
| 1862 |
const payload={
|
| 1863 |
item_type:meta?.item_type||'',
|
| 1864 |
item_name:itemName,
|
|
|
|
| 1868 |
target_level:parseInt(document.getElementById('up-target-level')?.value,10)||1,
|
| 1869 |
target_count:parseInt(document.getElementById('up-target-count')?.value,10)||1,
|
| 1870 |
elf_route:kind==='elf'&&route?route.key:null,
|
| 1871 |
+
player_level:parseInt(document.getElementById('up-player-level')?.value,10)||0,
|
| 1872 |
+
pet_use_gold:document.getElementById('up-pet-cost')?.value==='gold',
|
| 1873 |
+
pet_helper_odds:!!document.getElementById('up-helper-odds')?.checked,
|
| 1874 |
};
|
| 1875 |
try{
|
| 1876 |
const data=await post('/upgrade-lab',payload);
|
|
|
|
| 1898 |
? `${row.merge_required}x required`
|
| 1899 |
: row.action==='merge+dwarf'
|
| 1900 |
? `${row.merge_required}x merge, leftovers dwarf ${Number(row.dwarf_chance_pct||0).toFixed(2)}%`
|
| 1901 |
+
: row.action==='enhance'
|
| 1902 |
+
? `${row.merge_required}x per try, ${Number(row.chance_pct||0).toFixed(2)}%${row.gold_cost?`, gold ${row.gold_cost_display}`:''}`
|
| 1903 |
: row.action==='elf'
|
| 1904 |
? '10x Lv5, 80%, fail destroys inputs'
|
| 1905 |
: `${Number(row.chance_pct||0).toFixed(2)}% success`;
|
| 1906 |
return `<tr>
|
| 1907 |
<td>${_escHtml(from)} -> ${_escHtml(to)}</td>
|
| 1908 |
+
<td style="color:${row.action==='merge'?'var(--green)':row.action==='merge+dwarf'?'var(--gold)':row.action==='elf'||row.action==='enhance'?'var(--gold)':'var(--accent)'}">${_escHtml(action)}</td>
|
| 1909 |
<td>${_escHtml(rule)}</td>
|
| 1910 |
<td>${_numText(row.expected_input,3)}</td>
|
| 1911 |
<td>${_numText(row.expected_output,3)}</td>
|
tests/test_market_optimizer_logic.py
CHANGED
|
@@ -994,11 +994,40 @@ class MarketOptimizerLogicTests(unittest.TestCase):
|
|
| 994 |
self.assertAlmostEqual(distribution[1], 0.8)
|
| 995 |
self.assertAlmostEqual(result["plan"]["expected_count"], 0.8)
|
| 996 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 997 |
def test_frontend_flame_solver_uses_explicit_solved_flag(self):
|
| 998 |
html = (Path(__file__).resolve().parents[1] / "static" / "index.html").read_text(encoding="utf-8")
|
| 999 |
self.assertIn("let solved=false;", html)
|
| 1000 |
self.assertIn("solved=true;break;", html)
|
| 1001 |
self.assertNotIn("if(ticks===0){", html)
|
|
|
|
| 1002 |
|
| 1003 |
def test_market_selection_keeps_current_setup_as_search_seed(self):
|
| 1004 |
accounts = [self.account("A"), self.account("B")]
|
|
|
|
| 994 |
self.assertAlmostEqual(distribution[1], 0.8)
|
| 995 |
self.assertAlmostEqual(result["plan"]["expected_count"], 0.8)
|
| 996 |
|
| 997 |
+
def test_pet_enhancer_uses_level_gold_chance_table(self):
|
| 998 |
+
result = self.app._upgrade_lab_calculate(self.app.UpgradeLabInput(
|
| 999 |
+
item_name="Inferno",
|
| 1000 |
+
counts={"1": 20},
|
| 1001 |
+
target_kind="pet",
|
| 1002 |
+
target_count=1,
|
| 1003 |
+
player_level=10001,
|
| 1004 |
+
pet_use_gold=True,
|
| 1005 |
+
))
|
| 1006 |
+
chance = 0.1238
|
| 1007 |
+
self.assertEqual(result["goal"]["result_name"], "XInferno")
|
| 1008 |
+
self.assertEqual(result["pet"]["attempts"], 2)
|
| 1009 |
+
self.assertAlmostEqual(result["pet"]["chance"], chance)
|
| 1010 |
+
self.assertAlmostEqual(result["plan"]["expected_count"], 2 * chance)
|
| 1011 |
+
self.assertAlmostEqual(result["plan"]["probability"], 1 - (1 - chance) ** 2)
|
| 1012 |
+
|
| 1013 |
+
def test_pet_enhancer_accepts_result_name_as_lookup_alias(self):
|
| 1014 |
+
result = self.app._upgrade_lab_calculate(self.app.UpgradeLabInput(
|
| 1015 |
+
item_name="XInferno",
|
| 1016 |
+
counts={"1": 10},
|
| 1017 |
+
target_kind="pet",
|
| 1018 |
+
target_count=1,
|
| 1019 |
+
pet_helper_odds=True,
|
| 1020 |
+
))
|
| 1021 |
+
self.assertEqual(result["item"]["name"], "Inferno")
|
| 1022 |
+
self.assertEqual(result["goal"]["label"], "XInferno")
|
| 1023 |
+
self.assertEqual(result["pet"]["source_count"], 10)
|
| 1024 |
+
|
| 1025 |
def test_frontend_flame_solver_uses_explicit_solved_flag(self):
|
| 1026 |
html = (Path(__file__).resolve().parents[1] / "static" / "index.html").read_text(encoding="utf-8")
|
| 1027 |
self.assertIn("let solved=false;", html)
|
| 1028 |
self.assertIn("solved=true;break;", html)
|
| 1029 |
self.assertNotIn("if(ticks===0){", html)
|
| 1030 |
+
self.assertIn('<option value="pet">Pet Enhancer</option>', html)
|
| 1031 |
|
| 1032 |
def test_market_selection_keeps_current_setup_as_search_seed(self):
|
| 1033 |
accounts = [self.account("A"), self.account("B")]
|