diff --git a/.gitignore b/.gitignore index 67092b8..b29113d 100644 --- a/.gitignore +++ b/.gitignore @@ -139,4 +139,5 @@ dmypy.json cython_debug/ # diskcache folder -cache/ \ No newline at end of file +cache/ +temp/ \ No newline at end of file diff --git a/database/db_controller.py b/database/db_controller.py index c888de0..910175d 100644 --- a/database/db_controller.py +++ b/database/db_controller.py @@ -504,4 +504,106 @@ class DBController: # Fetch and print the results results = self.cursor.fetchall() - return [dict(row) for row in results] \ No newline at end of file + return [dict(row) for row in results] + + def get_all_encounters(self, type=None): + query = ''' + SELECT * FROM encounters + ''' + if type: + query += f"WHERE type = '{type}'" + + self.cursor.execute(query) + + # Fetch and print the results + results = self.cursor.fetchall() + return [dict(row) for row in results] + + def get_pokemon_by_generation(self, generation): + self.cursor.execute(f"SELECT * FROM pokemon_forms WHERE JSON_EXTRACT(data, '$.generation') = {generation}") + results = self.cursor.fetchall() + processed_data = [] + pre_processed_data = [dict(row) for row in results] + for row in pre_processed_data: + data = {} + data["PFIC"] = row["PFIC"] + if row["data"] and row["data"] != "": + data.update(json.loads(row["data"])) + + processed_data.append(data) + + return processed_data + + def find_most_distant_predecessors(self, node): + """ + Finds the most distant predecessor(s) of a given node in a DAG. + + Parameters: + G (networkx.DiGraph): The directed graph. + node (hashable): The node from which to find the most distant predecessors. + + Returns: + tuple: A tuple containing: + - A list of the most distant predecessor node(s). + - The distance (number of edges) to the most distant predecessor(s). + """ + if node not in self.graph: + raise ValueError(f"The node {node} is not in the graph.") + + # Reverse the graph to make predecessors into successors + G_rev = self.graph.reverse(copy=False) + + # Compute longest path lengths from the node in the reversed graph + dist = {} + # Perform topological sort on the reversed graph + topo_order = list(nx.topological_sort(G_rev)) + + # Find nodes reachable from the given node in the reversed graph + reachable_nodes = set(nx.descendants(G_rev, node)) + reachable_nodes.add(node) + + # Filter the topological order to include only reachable nodes + topo_order = [n for n in topo_order if n in reachable_nodes] + + # Initialize distances with negative infinity for all nodes + for n in reachable_nodes: + dist[n] = float('-inf') + dist[node] = 0 # Distance to the starting node is zero + + # Dynamic programming to compute longest path lengths + for u in topo_order: + for v in G_rev.successors(u): + if dist[v] < dist[u] + 1: + dist[v] = dist[u] + 1 + + if not dist: + return [], 0 # No predecessors found + + # Find the maximum distance + max_distance = max(dist.values()) + + # Identify the node(s) with the maximum distance + most_distant_predecessors = [n for n, d in dist.items() if d == max_distance] + + return most_distant_predecessors, max_distance + + def get_pokemon_home_list(self): + self.cursor.execute(''' + SELECT * + FROM pokemon_forms + WHERE JSON_EXTRACT(data, '$.storable_in_home') == true + GROUP BY json_extract(data, '$.sprite_url') + ORDER BY PFIC + ''') + results = self.cursor.fetchall() + processed_data = [] + pre_processed_data = [dict(row) for row in results] + for row in pre_processed_data: + data = {} + data["PFIC"] = row["PFIC"] + if row["data"] and row["data"] != "": + data.update(json.loads(row["data"])) + + processed_data.append(data) + + return processed_data diff --git a/ui/main_window_controller.py b/ui/main_window_controller.py index 12be192..ec54237 100644 --- a/ui/main_window_controller.py +++ b/ui/main_window_controller.py @@ -9,6 +9,7 @@ from ui.workers.gather_home_storage_status_worker import GatherHomeStorageStatus from ui.workers.gather_pokemon_forms_worker import GatherPokemonFormsWorker from ui.workers.gather_evolutions_worker import GatherEvolutions +from ui.workers.generate_plan_worker import GeneratePlanWorker from utility.functions import get_display_name from db import db @@ -208,3 +209,11 @@ class MainWindowController: def load_encounter_locations(self, pfic): encounters = db.get_encounters(pfic) self.view.update_encounter_list(encounters, pfic) + + def generate_plan(self): + worker = GeneratePlanWorker() + worker.signals.finished.connect(self.on_plan_generated) + self.thread_pool.start(worker) + + def on_plan_generated(self, data): + print("Plans Done!") diff --git a/ui/main_window_view.py b/ui/main_window_view.py index 3454aa0..3d430c8 100644 --- a/ui/main_window_view.py +++ b/ui/main_window_view.py @@ -26,6 +26,7 @@ class PokemonUI(QWidget): self.setup_main_tab() self.setup_db_operations_tab() self.setup_manage_encounters_tab() + self.setup_plan_tab() self.save_button = QPushButton("Save Changes") self.save_button.clicked.connect(self.controller.save_changes) @@ -217,7 +218,16 @@ class PokemonUI(QWidget): self.manage_encounters_tab.addLayout(left_layout, 1) self.manage_encounters_tab.addLayout(right_layout, 2) - #self.load_exclusive_sets() + def setup_plan_tab(self): + plan_tab = QWidget() + plan_tab_layout = QVBoxLayout(plan_tab) + self.tab_widget.addTab(plan_tab, "Plan Generation") + + plan_tab_layout.addStretch(1) + + generate_plan__btn = QPushButton("Generate Plan") + generate_plan__btn.clicked.connect(self.controller.generate_plan) + plan_tab_layout.addWidget(generate_plan__btn) def update_pokemon_forms(self, data): self.pokemon_list.clear() diff --git a/ui/workers/generate_plan_worker.py b/ui/workers/generate_plan_worker.py new file mode 100644 index 0000000..6bd1d37 --- /dev/null +++ b/ui/workers/generate_plan_worker.py @@ -0,0 +1,367 @@ +from PyQt6.QtCore import QObject, pyqtSignal, QRunnable +import json + +from cache import cache +from db import db + +from utility.data import exclusive_choice_pokemon +from utility.functions import get_shiftable_forms, parse_pfic + +class GeneratePlanWorkerSignals(QObject): + finished = pyqtSignal(dict) + +class GeneratePlanWorker(QRunnable): + def __init__(self): + super().__init__() + self.signals = GeneratePlanWorkerSignals() + self.caught_pokemon = {} + self.group_plan = [] + self.game_plan = [] + + def run(self): + try: + gathered_data = self.generate_plan() + self.signals.finished.emit(gathered_data) + except Exception as e: + print(f"Error gathering Pokémon home storage status: {e}") + + def generate_plan(self): + generational_groups = [1, 2], [3, 4, 5], [6], [7], [8], [9] + + for group in generational_groups: + group_plan = self.plan_for_group(group) + self.group_plan.append(group_plan) + + for group in self.group_plan: + for game in group: + game_plan = self.plan_for_game(game, group[game]) + self.game_plan.append(game_plan) + + storable_in_home = db.get_pokemon_home_list() + + total_accounted_for = 0 + for entry in self.game_plan: + total_accounted_for += len(entry["pokemon_map"]) + + total_needed = len(storable_in_home) + + for home_pokemon in storable_in_home: + found = False + for game in self.game_plan: + if home_pokemon["PFIC"] in game["pokemon_map"]: + found = True + break + if not found: + print(home_pokemon) + + return {} + + def plan_for_group(self, group): + group_plan = [] + needed_pokemon = set() + games_in_group = [] + game_pokemon = {} + + for generation in group: + games_in_group.extend(db.get_games_by_generation(generation)) + pokemon_in_generation = db.get_pokemon_by_generation(generation) + for pokemon in pokemon_in_generation: + if pokemon["storable_in_home"] == False: + continue + + pfic = pokemon["PFIC"] + + testing = [ + "0140-01-000-1", "0138-01-000-1", "0140-01-000-2", "0138-01-000-2", + "0141-01-000-1", "0139-01-000-1", "0141-01-000-2", "0139-01-000-2", + ] + + #if pfic not in testing: + # continue + + if pokemon["gender_relevant"] or pokemon["PFIC"][-1] == '0' or not any(pokemon["PFIC"][:-1] == s[:-1] for s in needed_pokemon): + needed_pokemon.add(pfic) + + random = db.get_all_encounters("random") + static = db.get_all_encounters("static") + starter = db.get_all_encounters("starter") + encounters = [] + encounters.extend(random) + encounters.extend(static) + encounters.extend(starter) + + for game in games_in_group: + game_pokemon[game['id']] = set() + for encounter in encounters: + if game["id"] == encounter["game_id"]: + current_pfic = encounter['PFIC'] + game_pokemon[game['id']].add(current_pfic) + evolution_chains = db.get_full_evolution_paths(current_pfic) + if evolution_chains and (len(evolution_chains["predecessors"]) > 0 or len(evolution_chains["successors"]) > 0): + for chains in evolution_chains["predecessors"]: + for evolved_pfic, method in chains: + if evolved_pfic in needed_pokemon: + game_pokemon[game['id']].add(evolved_pfic) + + for chains in evolution_chains["successors"]: + for evolved_pfic, method in chains: + if evolved_pfic in needed_pokemon: + game_pokemon[game['id']].add(evolved_pfic) + + selected_games = [] + catch_in_game = {} + remaining_pokemon = needed_pokemon.copy() + available_games = games_in_group.copy() + while remaining_pokemon: + cache = {} + for game in available_games: + cache[game['id']] = self.get_covered_pokemon(game, remaining_pokemon, game_pokemon[game['id']]) + + best_game = max(available_games, key=lambda g: len(cache[g["id"]])) + pokemon_covered = cache[best_game['id']] + + if not pokemon_covered: + print("No more Pokémon can be covered. Breaking loop.") + break + + selected_games.append(best_game) + catch_in_game[best_game["id"]] = pokemon_covered + remaining_pokemon -= pokemon_covered + + available_games.remove(best_game) + + for game in selected_games[:-1]: + + if game['id'] in cache and all(pokemon in cache[best_game['id']] + for pokemon in cache[game['id']] ): + selected_games.remove(game) + print(f"Removed {game['name']} as it's covered by {best_game['name']}") + + for game in selected_games: + output_file = "./temp/"+game["name"]+".json" + with open(output_file, 'w', encoding='utf-8') as f: + temp = [] + for pfic in catch_in_game[game["id"]]: + deets = db.get_pokemon_details(pfic, ["pfic", "name", "form_name"]) + temp.append(deets) + temp.sort(key=lambda x: parse_pfic(x["pfic"])) + json.dump(temp, f, indent=4, ensure_ascii=False) + pass + + return catch_in_game + + def get_covered_pokemon(self, game, needed_pokemon, available_pokemon): + exclusive_groups = self.get_exclusive_groups(game['id']) + pokemon_covered = set() + + evolution_paths = {} + evolution_predecessors = {} + all_pokemon = needed_pokemon | available_pokemon + + for group in exclusive_groups: + for pfic in group: + all_pokemon.add(pfic) + + for pfic in all_pokemon: + chain = db.get_full_evolution_paths(pfic) + evolution_paths[pfic] = chain + + predecessors = set([pfic]) + if chain and chain.get("predecessors"): + for entry in chain["predecessors"]: + for evolved_pfic, _ in entry: + predecessors.add(evolved_pfic) + evolution_predecessors[pfic] = predecessors + + exclusive_group_predecessors = [] + for group in exclusive_groups: + group_pokemon = set() + for pfic in group: + group_pokemon.update(evolution_predecessors.get(pfic, set())) + exclusive_group_predecessors.append(group_pokemon) + + def record_pokemon(pfic, container, needed): + container.add(pfic) + evolution_chains = evolution_paths.get(pfic) + if evolution_chains and (evolution_chains.get("predecessors") or evolution_chains.get("successors")): + for chains in evolution_chains.get("predecessors", []): + for evolved_pfic, _ in chains: + if evolved_pfic in needed: + container.add(evolved_pfic) + for chains in evolution_chains.get("successors", []): + for evolved_pfic, _ in chains: + if evolved_pfic in needed: + container.add(evolved_pfic) + + for pfic in needed_pokemon & available_pokemon: + if pfic in pokemon_covered: + continue + previous_evolutions = evolution_predecessors.get(pfic, set()) + is_exclusive = False + for group_pokemon in exclusive_group_predecessors: + if previous_evolutions & group_pokemon: + is_exclusive = True + if not pokemon_covered & group_pokemon: + record_pokemon(pfic, pokemon_covered, needed_pokemon) + break + if not is_exclusive: + record_pokemon(pfic, pokemon_covered, needed_pokemon) + + return pokemon_covered + + def get_exclusive_groups(self, game_id): + for data in exclusive_choice_pokemon: + if data["game_id"] == game_id: + return data["choices"] + return [] + + def plan_for_game(self, game_id, required_pokemon): + game = db.get_game_by_id(game_id) + game_plan = { + "game_name": game['name'], + "pokemon_to_catch": {}, + "pokemon_to_breed": {}, + "pokemon_map": {} + } + + print(f'Processing {game['name']}') + + random = db.get_all_encounters("random") + static = db.get_all_encounters("static") + starter = db.get_all_encounters("starter") + encounters = [] + encounters.extend(random) + encounters.extend(static) + encounters.extend(starter) + + encounters_in_game = {} + + for encounter in encounters: + if game["id"] == encounter["game_id"]: + encounters_in_game[encounter["PFIC"]] = encounter + + pokemon_to_catch = {} + pokemon_to_breed = {} + pokemon_map = {} + + def record_catch(pfic, gender, to_get): + if pfic not in encounters_in_game: + pass + + if pfic not in pokemon_to_catch: + pokemon_to_catch[pfic] = {} + + data = pokemon_to_catch[pfic] + if gender not in data: + data[gender] = 1 + else: + data[gender] += 1 + + if to_get not in pokemon_map: + pokemon_map[to_get] = {} + + pokemon_map[to_get]["ByEvolving"] = pfic + + def record_breed(pfic, gender, to_get): + if pfic not in pokemon_to_breed: + pokemon_to_breed[pfic] = {} + + data = pokemon_to_breed[pfic] + if gender not in data: + data[gender] = 1 + else: + data[gender] += 1 + + if to_get not in pokemon_map: + pokemon_map[to_get] = {} + + pokemon_map[to_get]["ByBreeding"] = pfic + + # TODO: Move this to a last pass + #if pfic not in pokemon_to_catch: + # record_catch(pfic, gender) + + def get_gender_string(value): + value_map = {"0": "Any", "1": "Male", "2": "Female"} + return value_map.get(str(value), "Unknown") + + missing_count = 0 + + for pfic in required_pokemon: + pokemon_data = db.get_pokemon_details(pfic) + evolution_chain = db.get_full_evolution_paths(pfic) + if evolution_chain and not any(evolution_chain["predecessors"]): + if pokemon_data["is_baby_form"]: + recorded = False + evolutions = db.get_evolution_graph(pfic) + for evolution in evolutions: + if evolution in encounters_in_game: + bucket = get_gender_string(0) + record_breed(evolution, bucket, pfic) + recorded = True + if not recorded: + if pfic in encounters_in_game: + bucket = get_gender_string(pfic[-1]) + record_catch(pfic, bucket, pfic) + else: + if pokemon_data["gender_relevant"]: + bucket = get_gender_string(pfic[-1]) + record_catch(pfic, bucket, pfic) + else: + shiftable_forms = get_shiftable_forms(pfic) + if len(shiftable_forms) > 0: + shiftable_pfic = shiftable_forms[0]["to_pfic"] + record_catch(shiftable_pfic, "Any", pfic) + else: + record_catch(pfic, "Any", pfic) + elif evolution_chain: + bucket = get_gender_string(0) + if pokemon_data["gender_relevant"]: + bucket = get_gender_string(pfic[-1]) + first_form = db.find_most_distant_predecessors(pfic) + if first_form: + first_form_pfic = first_form[0][0] + first_form_data = db.get_pokemon_details(first_form_pfic) + if first_form_data["is_baby_form"] == False: + shiftable_forms = get_shiftable_forms(first_form_pfic) + if len(shiftable_forms) > 0: + shiftable_pfic = shiftable_forms[0]["to_pfic"] + record_catch(shiftable_pfic, bucket, pfic) + else: + record_catch(first_form_pfic, bucket, pfic) + else: + recorded = False + evolutions = db.get_evolution_graph(first_form_pfic) + for evolution in evolutions: + if evolution in encounters_in_game: + record_catch(evolution, bucket, pfic) + recorded = True + if not recorded: + if first_form_pfic in encounters_in_game: + bucket = get_gender_string(pfic[-1]) + record_catch(first_form_pfic, bucket, pfic) + + total_catch = 0 + for sub_dict in pokemon_to_catch.values(): + total_catch += sum(sub_dict.values()) + + total_breed = 0 + for sub_dict in pokemon_to_breed.values(): + total_breed += sum(sub_dict.values()) + + all = total_catch + total_breed + missing_count + + sorted_keys = sorted(pokemon_to_catch.keys()) + + # Create a new dictionary with sorted keys + sorted_dict = {key: pokemon_to_catch[key] for key in sorted_keys} + game_plan["pokemon_to_catch"] = pokemon_to_catch + game_plan["pokemon_to_breed"] = pokemon_to_breed + game_plan["pokemon_map"] = pokemon_map + + for required in required_pokemon: + if required not in pokemon_map: + pokemon_data = db.get_pokemon_details(required) + print(pokemon_data["name"]) + + return game_plan \ No newline at end of file diff --git a/utility/data.py b/utility/data.py index 46008a3..a61ee6e 100644 --- a/utility/data.py +++ b/utility/data.py @@ -420,6 +420,30 @@ shiftable_forms = [ {"from_pfic":"0492-04-002-0", "to_pfic":"0492-04-001-0"} ] +exclusive_choice_pokemon = [ + { + "game_id": 1, + "choices":[ + ["0106-01-000-1", "0107-01-000-1"], #hitmonlee, hitmonchan + ["0140-01-000-1", "0138-01-000-1", "0140-01-000-2", "0138-01-000-2"] #Omanyte, Kabuto + ] + }, + { + "game_id": 2, + "choices":[ + ["0106-01-000-1", "0107-01-000-1"], #hitmonlee, hitmonchan + ["0140-01-000-1", "0138-01-000-1", "0140-01-000-2", "0138-01-000-2"] #Omanyte, Kabuto + ] + }, + { + "game_id": 3, + "choices":[ + ["0106-01-000-1", "0107-01-000-1"], #hitmonlee, hitmonchan + ["0140-01-000-1", "0138-01-000-1", "0140-01-000-2", "0138-01-000-2"] #Omanyte, Kabuto + ] + } +] + alcremie_forms = [ "Caramel Swirl Berry Sweet", "Caramel Swirl Clover Sweet",