diff --git a/mysite/flask_app.py b/mysite/flask_app.py index 5cec1c065..9886b1bb8 100644 --- a/mysite/flask_app.py +++ b/mysite/flask_app.py @@ -52,6 +52,8 @@ ) from utils.template_filters import * +from w1.owl import runDynamicOwlCalc + logger = get_logger(__name__) @@ -308,6 +310,13 @@ def logtest(): return "Hello, World!" +@app.route("/dynamic-owl-calc", methods=["GET"]) +def dynamic_owl_calc() -> Response | str: + output = '\n'.join(runDynamicOwlCalc(request.args)) + + return output.replace('\n', '
') + + def autoReviewBot( capturedCharacterInput, source_string diff --git a/mysite/models/account_parser.py b/mysite/models/account_parser.py index 375c1f58e..c4083a63b 100644 --- a/mysite/models/account_parser.py +++ b/mysite/models/account_parser.py @@ -1235,10 +1235,17 @@ def _parse_w1_owl(account): logger.warning(f"Owl data not present{', as expected' if account.version < 217 else ''}.") account.owl = { 'Discovered': safer_get(account.raw_optlacc_dict, 265, False), + 'Feathers': safer_get(account.raw_optlacc_dict, 253, 0), 'FeatherGeneration': safer_get(account.raw_optlacc_dict, 254, 0), 'BonusesOfOrion': safer_get(account.raw_optlacc_dict, 255, 0), + 'FeatherMultiplier': safer_get(account.raw_optlacc_dict, 256, 0), + 'FeatherCheapener': safer_get(account.raw_optlacc_dict, 257, 0), 'FeatherRestarts': safer_get(account.raw_optlacc_dict, 258, 0), - 'MegaFeathersOwned': safer_get(account.raw_optlacc_dict, 262, 0) + 'SuperFeatherProduction': safer_get(account.raw_optlacc_dict, 259, 0), + 'ShinyFeatherLevel': safer_get(account.raw_optlacc_dict, 260, 0), + 'SuperFeatherCheapener': safer_get(account.raw_optlacc_dict, 261, 0), + 'MegaFeathersOwned': safer_get(account.raw_optlacc_dict, 262, 0), + 'ShinyFeathers': safer_get(account.raw_optlacc_dict, 264, 0) } def _parse_w1_statues(account): diff --git a/mysite/w1/owl.py b/mysite/w1/owl.py index d01066c98..234490701 100644 --- a/mysite/w1/owl.py +++ b/mysite/w1/owl.py @@ -1,5 +1,5 @@ from models.models import Advice, AdviceGroup, AdviceSection -from consts.consts_autoreview import break_you_best, build_subgroup_label, EmojiType +from consts.consts_autoreview import break_you_best, build_subgroup_label, EmojiType, ValueToMulti from consts.progression_tiers import owl_progressionTiers, true_max_tiers from utils.misc.add_subgroup_if_available_slot import add_subgroup_if_available_slot from utils.logging import get_logger @@ -85,6 +85,101 @@ def getProgressionTiersAdviceGroup() -> tuple[AdviceGroup, int, int, int]: overall_SectionTier = min(true_max, tier_MegaFeathers) return tiers_ag, overall_SectionTier, max_tier, true_max +def getUpgradeSequenceLinks() -> AdviceGroup: + secret_owl_bonus = session_data.account.vault['Upgrades']['Go Go Secret Owl']['Total Value'] + gambit_bonus = ValueToMulti(session_data.account.caverns['Caverns']['Gambit']['Bonuses'][8]['Value']) + + def make_link(target, group_by_seconds, fresh_restart): + params = {} + params['target'] = target + params['mega_reset'] = session_data.account.owl['MegaFeathersOwned'] + if fresh_restart: + params['restart'] = session_data.account.owl['FeatherRestarts']+1 + else: + params['restart'] = session_data.account.owl['FeatherRestarts'] + params['shiny_feather_count'] = session_data.account.owl['ShinyFeathers'] + params['group_by_seconds'] = group_by_seconds + params['ignore_less_than'] = 0 # TODO: Remove? + params['gambit'] = gambit_bonus + params['secret_owl'] = secret_owl_bonus + params['orion'] = session_data.account.owl['BonusesOfOrion'] + if fresh_restart: + params['feather_gen'] = 1 + else: + params['feather_gen'] = session_data.account.owl['FeatherGeneration'] + params['feather_mult'] = session_data.account.owl['FeatherMultiplier'] + params['cheapener'] = session_data.account.owl['FeatherCheapener'] + params['super_production'] = session_data.account.owl['SuperFeatherProduction'] + params['shiny_feather_level'] = session_data.account.owl['ShinyFeatherLevel'] + params['super_cheapener'] = session_data.account.owl['SuperFeatherCheapener'] + params['feathers'] = session_data.account.owl['Feathers'] + + return 'dynamic-owl-calc?' + '&'.join(f'{key}={val}' for key, val in params.items()) + + linksDict = { + 'Restart Links': [ + Advice( + label=f'Optimize path to Restart', + picture_class='bonuses-of-orion' + ), + Advice( + label=f'Optimize path to Restart (group by 1 second)', + picture_class='bonuses-of-orion' + ), + Advice( + label=f'Optimize next Restart', + picture_class='bonuses-of-orion' + ), + Advice( + label=f'Optimize next Restart (group by 1 second)', + picture_class='bonuses-of-orion' + ), + ], + 'Mega Reset Links': [ + Advice( + label=f'Optimize path to Mega Reset', + picture_class='bonuses-of-orion' + ), + Advice( + label=f'Optimize path to Mega Reset (group by 1 second)', + picture_class='bonuses-of-orion' + ), + Advice( + label=f'Optimize path to Mega Reset (next restart)', + picture_class='bonuses-of-orion' + ), + Advice( + label=f'Optimize path to Mega Reset (group by 1 second, next restart)', + picture_class='bonuses-of-orion' + ), + ], + 'Bonuses of Orion Links': [ + Advice( + label=f'Optimize path to Bonuses of Orion', + picture_class='bonuses-of-orion' + ), + Advice( + label=f'Optimize path to Bonuses of Orion (group by 1 second)', + picture_class='bonuses-of-orion' + ), + Advice( + label=f'Optimize path to Bonuses of Orion (next restart)', + picture_class='bonuses-of-orion' + ), + Advice( + label=f'Optimize path to Bonuses of Orion (group by 1 second, next restart)', + picture_class='bonuses-of-orion' + ), + ] + } + + return AdviceGroup( + tier='', + pre_string='Upgrade Sequence Links', + advices=linksDict, + informational=True + ) + def getOwlAdviceSection() -> AdviceSection: # Generate Alert Advice getNoFeathersGeneratingAlert() @@ -92,6 +187,7 @@ def getOwlAdviceSection() -> AdviceSection: # Generate AdviceGroups owl_AdviceGroupDict = {} owl_AdviceGroupDict['MegaFeathers'], overall_SectionTier, max_tier, true_max = getProgressionTiersAdviceGroup() + owl_AdviceGroupDict['Links'] = getUpgradeSequenceLinks() # Generate AdviceSection @@ -107,3 +203,274 @@ def getOwlAdviceSection() -> AdviceSection: groups=owl_AdviceGroupDict.values() ) return owl_AdviceSection + +import collections +import functools + +OwlUpgrades = collections.namedtuple('OwlUpgrades', + ('feather_gen', 'orion', 'feather_mult', + 'cheapener', 'restart', 'super_production', + 'shiny_feather', 'super_cheapener', 'mega_reset')) + +UPGRADE_BASE_COST_FACTORS = OwlUpgrades(5, 350, 500, + 3000, 1000000, 2000000, + 5000000, 50000000, 250000000000) +UPGRADE_COST_EXP_BASES = OwlUpgrades(1.1, 25, 1.11, + 1.16, 14, 1.12, + 1.4, 1.27, 20) + +UPGRADE_NAMES = OwlUpgrades('Feather Generation', 'Bonuses of Orion', 'Feather Multiplier', + 'Feather Cheapener', 'Feather Restart', 'Super Feather Production', + 'Shiny Feather', 'Super Feather Cheapener', 'The Great Mega Reset') + +def _calc_feathers_per_second(upgrade_levels, shiny_feather_count, owl_multiplier): + feathers_per_sec = 0 + + feathers_per_sec += upgrade_levels.feather_gen + + if upgrade_levels.mega_reset >= 5: + feathers_per_sec += 2 * upgrade_levels.cheapener + feathers_per_sec += 4 * upgrade_levels.super_cheapener + + feathers_per_sec += 5 * upgrade_levels.super_production + + if upgrade_levels.feather_mult > 0: + feathers_per_sec *= 1 + (upgrade_levels.feather_mult * 0.05) + + if upgrade_levels.shiny_feather > 0 and shiny_feather_count > 0: + feathers_per_sec *= 1 + (upgrade_levels.shiny_feather * shiny_feather_count) / 100 + + if upgrade_levels.restart > 0: + if upgrade_levels.mega_reset >= 7: + feathers_per_sec *= (5 ** upgrade_levels.restart) + else: + feathers_per_sec *= (3 ** upgrade_levels.restart) + + if upgrade_levels.mega_reset >= 1: + feathers_per_sec *= 10 + + return feathers_per_sec * owl_multiplier + +def _calc_upgrade_costs(upgrade_levels): + upgrade_costs = [] + + discount = 1 + + # Cheapeners + discount *= 1 / (1 + upgrade_levels.cheapener/10) + discount *= 1 / (1 + upgrade_levels.super_cheapener/5) + + # Mega feather 3 - upgrades cost 1% less per feather gen level + if upgrade_levels.mega_reset >= 3: + discount *= 1 / (1 + upgrade_levels.feather_gen/100) + + for idx, count in enumerate(upgrade_levels): + base = UPGRADE_BASE_COST_FACTORS[idx] + exp_base = UPGRADE_COST_EXP_BASES[idx] + + if idx == 0: + # Feather gen is special + base *= count + if upgrade_levels.mega_reset >= 9: + exp_base = 1.075 + + upgrade_costs.append(base * pow(exp_base, count) * discount) + + return OwlUpgrades(*upgrade_costs) + +class OwlState: + def __init__(self, upgrade_levels, shiny_feather_count, owl_multiplier): + self.upgrade_levels = upgrade_levels + self.shiny_feather_count = shiny_feather_count + self.owl_multiplier = owl_multiplier + + self.feathers_per_second = _calc_feathers_per_second(upgrade_levels, + shiny_feather_count, + owl_multiplier) + + self.upgrade_costs = _calc_upgrade_costs(upgrade_levels) + + def time_to_upgrade_by_idx(self, upgrade_idx, current_feathers=0): + return max((self.upgrade_costs[upgrade_idx] - current_feathers) / self.feathers_per_second, + 0) + + def time_to_upgrade(self, upgrade_name, current_feathers=0): + return max((getattr(self.upgrade_costs, upgrade_name) - current_feathers) / self.feathers_per_second, + 0) + + def time_to_orion(self, current_feathers=0): + return self.time_to_upgrade('orion', current_feathers) + + def time_to_restart(self, current_feathers=0): + return self.time_to_upgrade('restart', current_feathers) + + def time_to_mega_reset(self, current_feathers=0): + return self.time_to_upgrade('mega_reset', current_feathers) + + # This contract is maybe icky, operating by index + def return_upgraded_state(self, idx): + name = OwlUpgrades._fields[idx] + count = self.upgrade_levels[idx] + d = {name: count+1} + return OwlState(self.upgrade_levels._replace(**d), + self.shiny_feather_count, + self.owl_multiplier) + +def _optimize_upgrades(state, target, current_feathers, group_by_seconds, ignore_less_than): + theoretical_time = 0 + upgrades = [] + + while True: + time_to_upgrade = state.time_to_upgrade(target, current_feathers) + + best_improvement_ratio = 0 + this_upgrade_time = None + next_state = None + next_current_feathers = None + this_name = None + this_idx = None + + for idx in range(9): + if idx in (1, 4, 8): + continue # Don't try to upgrade orion, restart, or reset + + upgrade_time = state.time_to_upgrade_by_idx(idx, current_feathers) + + if upgrade_time >= time_to_upgrade: + continue + + test_state = state.return_upgraded_state(idx) + test_current_feathers = max(current_feathers - state.upgrade_costs[idx], 0) + total_time = upgrade_time + test_state.time_to_upgrade(target, test_current_feathers) + + if total_time >= time_to_upgrade: + continue + + savings = time_to_upgrade - total_time + if savings < ignore_less_than: + continue + + improvement_ratio = savings / state.upgrade_costs[idx] + + if improvement_ratio > best_improvement_ratio: + best_improvement_ratio = improvement_ratio + next_state = test_state + next_current_feathers = test_current_feathers + this_upgrade_time = upgrade_time + this_name = OwlUpgrades._fields[idx] + this_idx = idx + + if next_state is None: + return theoretical_time + state.time_to_upgrade(target, current_feathers), state, upgrades + + state = next_state + current_feathers = next_current_feathers + theoretical_time += this_upgrade_time + upgrades.append((this_name, this_upgrade_time)) + + # Keep upgrade while each level is + # group_by_seconds: + break + + test_state = state.return_upgraded_state(this_idx) + test_current_feathers = max(current_feathers - state.upgrade_costs[idx], 0) + total_time = upgrade_time + test_state.time_to_upgrade(target, test_current_feathers) + + if total_time >= time_to_upgrade: + break + + savings = time_to_upgrade - total_time + if savings < ignore_less_than: + break + + state = test_state + current_feathers = test_current_feathers + theoretical_time += upgrade_time + upgrades.append((this_name, upgrade_time)) + +def runDynamicOwlCalc(request_args): + target = request_args['target'] + + target_pretty_name = { + 'restart': 'Restart', + 'mega_reset': 'Mega Reset', + 'orion': 'Bonuses of Orion', + }[target] + + initial_upgrade_levels = OwlUpgrades( + int(request_args.get('feather_gen', '0')), + int(request_args.get('orion', '0')), + int(request_args.get('feather_mult', '0')), + int(request_args.get('cheapener', '0')), + int(request_args.get('restart', '0')), + int(request_args.get('super_production', '0')), + int(request_args.get('shiny_feather_level', '0')), + int(request_args.get('super_cheapener', '0')), + int(request_args.get('mega_reset', '0')) + ) + shiny_feather_count = int(request_args.get('shiny_feather_count', '0')) + secret_owl = float(request_args.get('secret_owl', '0')) + gambit = float(request_args.get('gambit', '1.0')) + current_feathers = int(float(request_args.get('feathers', '0'))) + + group_by_seconds = float(request_args.get('group_by_seconds', '1')) + ignore_less_than = float(request_args.get('ignore_less_than', '0')) # TODO: Drop? + + owl_multiplier = gambit * (1 + secret_owl / 100) + + state = OwlState(initial_upgrade_levels, shiny_feather_count, owl_multiplier) + + yield f'Current time to {target_pretty_name}:' + + seconds = state.time_to_upgrade(target, current_feathers) + minutes = seconds / 60 + hours = minutes / 60 + days = hours / 24 + + yield f'{seconds} seconds' + yield f'{minutes} minutes' + yield f'{hours} hours' + yield f'{days} days' + + theory_time, optimized, upgrade_order = _optimize_upgrades(state, target, current_feathers, group_by_seconds, ignore_less_than) + + yield '' + yield '' + yield 'Target upgrades:' + for name, count in zip(UPGRADE_NAMES, optimized.upgrade_levels): + yield f'{name}: {count}' + + yield '' + yield '' + yield f'Theoretical time: {theory_time} seconds' + yield f'Theoretical time: {theory_time/60} minutes' + yield f'Theoretical time: {theory_time/60/60} hours' + yield f'Theoretical time: {theory_time/60/60/24} days' + + yield '' + yield 'Upgrade path:' + + last_upgrade = None + current_delta = 0 + upgrade_counts = collections.Counter() + + for name, count in zip(OwlUpgrades._fields, initial_upgrade_levels): + if count != 0: + upgrade_counts[name] = count + + for upgrade_name, _ in upgrade_order: + if upgrade_name != last_upgrade and last_upgrade is not None: + yield f'{last_upgrade} to {upgrade_counts[last_upgrade]} (delta of {current_delta})' + current_delta = 0 + last_upgrade = upgrade_name + upgrade_counts[upgrade_name] += 1 + current_delta += 1 + + if last_upgrade is not None: # In case of no upgrades + yield f'{last_upgrade} to {upgrade_counts[last_upgrade]} (delta of {current_delta})' + + # TODO: Return the upgrade sequence as JSON, including meta info such as "time left buying upgrades", "time to target upgrade", "remaining savings" + # TODO: Of course, this requires a proper UI exposure too!