From 481e3cb9d2f9b1e89512487660a515538660c1cb Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 6 Dec 2024 15:33:02 +0000 Subject: [PATCH] - Add in a new appraoch by splitting the large routes into stages --- routes/Gold_Silver_Route.py | 238 ++++++++++++++++++---------- routes/Red_Blue_Route.py | 20 ++- routes/game_world.py | 38 +++++ routes/pokemon_game_desc.py | 140 +--------------- routes/requirements.py | 37 +++++ routes/route_planner.py | 184 +++++++++++++++++++++ ui/main_window_controller.py | 4 +- ui/workers/generate_PDDLs_worker.py | 4 +- ui/workers/generate_plan_worker.py | 19 ++- ui/workers/route_solve_worker.py | 78 +++++++++ ui/workers/solve_pddl_route.py | 4 +- 11 files changed, 529 insertions(+), 237 deletions(-) create mode 100644 routes/game_world.py create mode 100644 routes/requirements.py create mode 100644 routes/route_planner.py create mode 100644 ui/workers/route_solve_worker.py diff --git a/routes/Gold_Silver_Route.py b/routes/Gold_Silver_Route.py index 6270be8..bd42ecd 100644 --- a/routes/Gold_Silver_Route.py +++ b/routes/Gold_Silver_Route.py @@ -1,6 +1,8 @@ +from types import MethodType import networkx as nx from networkx import Graph +from routes.game_world import RouteStage from routes.pokemon_game_desc import PokemonGameDesc NEW_BARK_TOWN = 'New Bark Town' @@ -98,28 +100,70 @@ FLY_OUT_OF_BATTLE = 'Fly out of battle' def get_gold_silver_desc() -> PokemonGameDesc: desc: PokemonGameDesc = PokemonGameDesc() - desc.graph = get_gold_silver_route() - desc.game_name = "Gold_Silver" - desc.towns_and_cities = [NEW_BARK_TOWN, CHERRYGROVE_CITY, VIOLET_CITY, AZALEA_TOWN, GOLDENROD_CITY, ECRUTEAK_CITY, OLIVINE_CITY, CIANWOOD_CITY, MAHOGANY_TOWN, BLACKTHORN_CITY, PALLET_TOWN, VIRIDIAN_CITY, PEWTER_CITY, CERULEAN_CITY, SAFFRON_CITY, CELADON_CITY, VERMILION_CITY, FUCHSIA_CITY, CINNABAR_ISLAND] - desc.badges = [ZEPHYR_BADGE, HIVE_BADGE, PLAIN_BADGE, FOG_BADGE, STORM_BADGE, MINERAL_BADGE, GLACIER_BADGE, RISING_BADGE, BOULDER_BADGE, CASCADE_BADGE, THUNDER_BADGE, RAINBOW_BADGE, MARSH_BADGE, SOUL_BADGE, VOLCANO_BADGE, EARTH_BADGE] + #desc.graph = get_gold_silver_route() + desc.game_name = "Crystal" + desc.towns_and_cities = [NEW_BARK_TOWN, CHERRYGROVE_CITY, VIOLET_CITY, AZALEA_TOWN, GOLDENROD_CITY, ECRUTEAK_CITY, OLIVINE_CITY, CIANWOOD_CITY, MAHOGANY_TOWN, BLACKTHORN_CITY] + #desc.badges = [ZEPHYR_BADGE, HIVE_BADGE, PLAIN_BADGE, FOG_BADGE, STORM_BADGE, MINERAL_BADGE, GLACIER_BADGE, RISING_BADGE, BOULDER_BADGE, CASCADE_BADGE, THUNDER_BADGE, RAINBOW_BADGE, MARSH_BADGE, SOUL_BADGE, VOLCANO_BADGE, EARTH_BADGE] desc.hms = [CUT, SURF, FLASH, STRENGTH, FLY, WATERFALL, WHIRLPOOL] desc.additional_goals = [WOKE_SNORLAX] - desc.starting_town = NEW_BARK_TOWN - desc.end_goal = MT_SILVER - desc.must_visit = set([MT_MOON]) + #desc.starting_town = NEW_BARK_TOWN + #desc.end_goal = MT_SILVER + #desc.must_visit = set([MT_MOON]) desc.games_covered = ["Gold", "Silver", "Crystal"] desc.file_name = "gold_silver_crystal_problem.pddl" desc.one_way_routes.append("Route_29 -> Route_46") + stage: RouteStage = RouteStage( + "Johto", + get_johto_route(), + NEW_BARK_TOWN, + INDIGO_PLATEAU, + [ZEPHYR_BADGE, HIVE_BADGE, PLAIN_BADGE, FOG_BADGE, STORM_BADGE, MINERAL_BADGE, GLACIER_BADGE, RISING_BADGE], + set(), + [NEW_BARK_TOWN, CHERRYGROVE_CITY, VIOLET_CITY, AZALEA_TOWN, GOLDENROD_CITY, ECRUTEAK_CITY, OLIVINE_CITY, CIANWOOD_CITY, MAHOGANY_TOWN, BLACKTHORN_CITY] + ) + desc.stages.append(stage) + + kanto_stage: RouteStage = RouteStage( + "Kanto", + get_kanto_route(), + NEW_BARK_TOWN, + INDIGO_PLATEAU, + [BOULDER_BADGE, CASCADE_BADGE, THUNDER_BADGE, RAINBOW_BADGE, MARSH_BADGE, SOUL_BADGE, VOLCANO_BADGE, EARTH_BADGE], + set(), + [NEW_BARK_TOWN, CHERRYGROVE_CITY, VIOLET_CITY, AZALEA_TOWN, GOLDENROD_CITY, ECRUTEAK_CITY, OLIVINE_CITY, CIANWOOD_CITY, MAHOGANY_TOWN, BLACKTHORN_CITY, PALLET_TOWN, VIRIDIAN_CITY, PEWTER_CITY, CERULEAN_CITY, SAFFRON_CITY, CELADON_CITY, VERMILION_CITY, FUCHSIA_CITY, CINNABAR_ISLAND] + ) + desc.stages.append(kanto_stage) + return desc -def get_gold_silver_route() -> Graph: - G = nx.Graph() +def get_johto_desc(): + desc: PokemonGameDesc = PokemonGameDesc() + #desc.graph = get_gold_silver_route() + desc.game_name = "Gold_Silver_Johto" + desc.towns_and_cities = [NEW_BARK_TOWN, CHERRYGROVE_CITY, VIOLET_CITY, AZALEA_TOWN, GOLDENROD_CITY, ECRUTEAK_CITY, OLIVINE_CITY, CIANWOOD_CITY, MAHOGANY_TOWN, BLACKTHORN_CITY] + #desc.badges = [ZEPHYR_BADGE, HIVE_BADGE, PLAIN_BADGE, FOG_BADGE, STORM_BADGE, MINERAL_BADGE, GLACIER_BADGE, RISING_BADGE] + desc.hms = [CUT, SURF, FLASH, STRENGTH, FLY, WATERFALL, WHIRLPOOL] + desc.additional_goals = [] + #desc.starting_town = NEW_BARK_TOWN + #desc.end_goal = INDIGO_PLATEAU + desc.must_visit = set() + desc.games_covered = ["Gold", "Silver", "Crystal"] + desc.file_name = "gold_silver_crystal_problem.pddl" + desc.one_way_routes.append("Route_29 -> Route_46") + + return desc - for i in range(1,46): +def get_johto_route(): + G = nx.Graph() + for i in range(29,46): G.add_node(f'Route {i}', node_type='route') + G.add_node(f'Route 26', node_type='route') + G.add_node(f'Route 27', node_type='route') + G.add_node(f'Route 23', node_type='route') + G.add_node(NEW_BARK_TOWN, node_type='location') G.add_node(CHERRYGROVE_CITY, node_type='location') G.add_node(VIOLET_CITY, node_type='location') @@ -146,24 +190,9 @@ def get_gold_silver_route() -> Graph: G.add_node(DARK_CAVE, node_type='location') G.add_node(VICTORY_ROAD, node_type='location') G.add_node(INDIGO_PLATEAU, node_type='location') - G.add_node(SS_AQUA, node_type='location') - G.add_node(VERMILION_CITY, node_type='location') - G.add_node(SAFFRON_CITY, node_type='location') - G.add_node(LAVENDER_TOWN, node_type='location') - G.add_node(ROCK_TUNNEL, node_type='location') - G.add_node(CERULEAN_CITY, node_type='location') - G.add_node(CELADON_CITY, node_type='location') - G.add_node(FUCHSIA_CITY, node_type='location') - G.add_node(DIGLETTS_CAVE, node_type='location') - G.add_node(PEWTER_CITY, node_type='location') - G.add_node(MT_MOON, node_type='location') - G.add_node(VIRIDIAN_CITY, node_type='location') - G.add_node(PALLET_TOWN, node_type='location') - G.add_node(CINNABAR_ISLAND, node_type='location') - G.add_node(MT_SILVER, node_type='location') G.add_node(SAFARI_ZONE, node_type='location') G.add_node(TOHJO_FALLS, node_type='location') - G.add_node(POWER_PLANT, node_type='location') + G.add_node(LEAGUE_RECEPTION_GATE, node_type='location') G.add_edge(NEW_BARK_TOWN, 'Route 29', condition=None) G.add_edge(CHERRYGROVE_CITY, 'Route 29', condition=None) @@ -220,66 +249,15 @@ def get_gold_silver_route() -> Graph: G.add_edge('Route 27', TOHJO_FALLS, condition=None) G.add_edge(LEAGUE_RECEPTION_GATE, 'Route 26', condition=None) - G.add_edge(LEAGUE_RECEPTION_GATE, 'Route 28', condition=[ROUTE_28_UNLOCKED]) - G.add_edge(LEAGUE_RECEPTION_GATE, 'Route 22', condition=[WOKE_SNORLAX]) + #G.add_edge(LEAGUE_RECEPTION_GATE, 'Route 28', condition=[ROUTE_28_UNLOCKED]) + #G.add_edge(LEAGUE_RECEPTION_GATE, 'Route 22', condition=[WOKE_SNORLAX]) G.add_edge(LEAGUE_RECEPTION_GATE, 'Route 23', condition=None) - G.add_edge('Route 28', MT_SILVER, condition=None) + #G.add_edge('Route 28', MT_SILVER, condition=None) G.add_edge('Route 23', VICTORY_ROAD, condition=[ZEPHYR_BADGE, HIVE_BADGE, PLAIN_BADGE, FOG_BADGE, STORM_BADGE, MINERAL_BADGE, GLACIER_BADGE, RISING_BADGE]) - - G.add_edge(SS_AQUA, VERMILION_CITY, condition=None) - G.add_edge(SAFFRON_CITY, GOLDENROD_CITY, condition=[POWER_RESTORED, RAIL_PASS]) - G.add_edge(DIGLETTS_CAVE, 'Route 11', condition=[WOKE_SNORLAX]) - - G.add_edge(PALLET_TOWN, 'Route 1', condition=None) - G.add_edge(PALLET_TOWN, 'Route 21', condition=[SURF]) - G.add_edge('Route 1', VIRIDIAN_CITY, condition=None) - G.add_edge(VIRIDIAN_CITY, 'Route 2', condition=None) - G.add_edge('Route 2', 'Viridian Forest', condition=None) - G.add_edge('Route 2', 'Route 3', condition=[CUT]) - G.add_edge('Viridian Forest', PEWTER_CITY, condition=None) - G.add_edge(PEWTER_CITY, 'Route 3', condition=None) - G.add_edge('Route 3', MT_MOON, condition=None) - G.add_edge(MT_MOON, 'Route 4', condition=None) - G.add_edge('Route 4', CERULEAN_CITY, condition=None) - G.add_edge(CERULEAN_CITY, 'Route 24', condition=None) - G.add_edge(CERULEAN_CITY, 'Route 9', condition=[CUT]) - G.add_edge(CERULEAN_CITY, 'Route 5', condition=None) - G.add_edge('Route 5', SAFFRON_CITY, condition=None) - G.add_edge(SAFFRON_CITY, 'Route 6', condition=None) - G.add_edge(SAFFRON_CITY, 'Route 7', condition=None) - G.add_edge(SAFFRON_CITY, 'Route 8', condition=None) - G.add_edge('Route 6', VERMILION_CITY, condition=None) - G.add_edge(VERMILION_CITY, 'Route 11', condition=None) - G.add_edge('Route 11', 'Route 12', condition=None) - G.add_edge('Route 11', DIGLETTS_CAVE, condition=None) - G.add_edge('Route 2', DIGLETTS_CAVE, condition=[CUT]) - G.add_edge('Route 12', 'Route 13', condition=None) - G.add_edge('Route 12', LAVENDER_TOWN, condition=None) - G.add_edge('Route 7', LAVENDER_TOWN, condition=None) - G.add_edge(LAVENDER_TOWN, 'Route 10', condition=None) - G.add_edge('Route 9', ROCK_TUNNEL, condition=[FLASH]) - G.add_edge('Route 10', ROCK_TUNNEL, condition=[FLASH]) - G.add_edge(CELADON_CITY, 'Route 8', condition=None) - G.add_edge(CELADON_CITY, 'Route 16', condition=None) - G.add_edge('Route 16', 'Route 17', condition=None) - G.add_edge('Route 17', 'Route 18', condition=None) - G.add_edge('Route 18', FUCHSIA_CITY, condition=None) - G.add_edge(FUCHSIA_CITY,'Route 19', condition=None) - G.add_edge(FUCHSIA_CITY,'Route 15', condition=None) - G.add_edge('Route 19', 'Seafoam Islands', condition=[SURF]) - G.add_edge('Seafoam Islands', 'Route 20', condition=[SURF]) - G.add_edge('Route 20', CINNABAR_ISLAND, condition=[SURF]) - G.add_edge('Route 21', CINNABAR_ISLAND, condition=[SURF]) - G.add_edge('Route 22', VIRIDIAN_CITY, condition=None) G.add_edge(VICTORY_ROAD, INDIGO_PLATEAU, condition=None) - G.add_edge('Route 12', 'Route 13', condition=None) - G.add_edge('Route 13', 'Route 14', condition=None) - G.add_edge('Route 14', 'Route 15', condition=None) - G.add_edge('Route 24', 'Route 25', condition=None) - G.add_edge('Route 10', POWER_PLANT, condition=[SURF]) - G.add_edge('Route 5', 'Underground Path', condition=None) - G.add_edge('Underground Path', 'Route 6', condition=None) + + #G.add_edge(SS_AQUA, VERMILION_CITY, condition=None) G.nodes[NEW_BARK_TOWN]['grants_conditions'] = [ {'condition': S_S_TICKET, 'required_conditions': [JOHTO_CHAMPION]} @@ -337,6 +315,93 @@ def get_gold_silver_route() -> Graph: G.nodes[INDIGO_PLATEAU]['grants_conditions'] = [ {'condition': JOHTO_CHAMPION, 'required_conditions': []} ] + + return G + +def get_kanto_route(): + G = nx.Graph() + + for i in range(1,23): + G.add_node(f'Route {i}', node_type='route') + + G.add_node(SS_AQUA, node_type='location') + G.add_node(VERMILION_CITY, node_type='location') + G.add_node(SAFFRON_CITY, node_type='location') + G.add_node(LAVENDER_TOWN, node_type='location') + G.add_node(ROCK_TUNNEL, node_type='location') + G.add_node(CERULEAN_CITY, node_type='location') + G.add_node(CELADON_CITY, node_type='location') + G.add_node(FUCHSIA_CITY, node_type='location') + G.add_node(DIGLETTS_CAVE, node_type='location') + G.add_node(PEWTER_CITY, node_type='location') + G.add_node(MT_MOON, node_type='location') + G.add_node(VIRIDIAN_CITY, node_type='location') + G.add_node(PALLET_TOWN, node_type='location') + G.add_node(CINNABAR_ISLAND, node_type='location') + G.add_node(MT_SILVER, node_type='location') + G.add_node(POWER_PLANT, node_type='location') + + G.add_edge(LEAGUE_RECEPTION_GATE, 'Route 28', condition=[ROUTE_28_UNLOCKED]) + G.add_edge(LEAGUE_RECEPTION_GATE, 'Route 22', condition=[WOKE_SNORLAX]) + G.add_edge(LEAGUE_RECEPTION_GATE, 'Route 23', condition=None) + + G.add_edge('Route 28', MT_SILVER, condition=None) + G.add_edge('Route 23', VICTORY_ROAD, condition=[ZEPHYR_BADGE, HIVE_BADGE, PLAIN_BADGE, FOG_BADGE, STORM_BADGE, MINERAL_BADGE, GLACIER_BADGE, RISING_BADGE]) + + G.add_edge(SS_AQUA, VERMILION_CITY, condition=None) + G.add_edge(SAFFRON_CITY, GOLDENROD_CITY, condition=[POWER_RESTORED, RAIL_PASS]) + G.add_edge(DIGLETTS_CAVE, 'Route 11', condition=[WOKE_SNORLAX]) + + G.add_edge(PALLET_TOWN, 'Route 1', condition=None) + G.add_edge(PALLET_TOWN, 'Route 21', condition=[SURF]) + G.add_edge('Route 1', VIRIDIAN_CITY, condition=None) + G.add_edge(VIRIDIAN_CITY, 'Route 2', condition=None) + G.add_edge('Route 2', 'Viridian Forest', condition=None) + G.add_edge('Route 2', 'Route 3', condition=[CUT]) + G.add_edge('Viridian Forest', PEWTER_CITY, condition=None) + G.add_edge(PEWTER_CITY, 'Route 3', condition=None) + G.add_edge('Route 3', MT_MOON, condition=None) + G.add_edge(MT_MOON, 'Route 4', condition=None) + G.add_edge('Route 4', CERULEAN_CITY, condition=None) + G.add_edge(CERULEAN_CITY, 'Route 24', condition=None) + G.add_edge(CERULEAN_CITY, 'Route 9', condition=[CUT]) + G.add_edge(CERULEAN_CITY, 'Route 5', condition=None) + G.add_edge('Route 5', SAFFRON_CITY, condition=None) + G.add_edge(SAFFRON_CITY, 'Route 6', condition=None) + G.add_edge(SAFFRON_CITY, 'Route 7', condition=None) + G.add_edge(SAFFRON_CITY, 'Route 8', condition=None) + G.add_edge('Route 6', VERMILION_CITY, condition=None) + G.add_edge(VERMILION_CITY, 'Route 11', condition=None) + G.add_edge('Route 11', 'Route 12', condition=None) + G.add_edge('Route 11', DIGLETTS_CAVE, condition=None) + G.add_edge('Route 2', DIGLETTS_CAVE, condition=[CUT]) + G.add_edge('Route 12', 'Route 13', condition=None) + G.add_edge('Route 12', LAVENDER_TOWN, condition=None) + G.add_edge('Route 7', LAVENDER_TOWN, condition=None) + G.add_edge(LAVENDER_TOWN, 'Route 10', condition=None) + G.add_edge('Route 9', ROCK_TUNNEL, condition=[FLASH]) + G.add_edge('Route 10', ROCK_TUNNEL, condition=[FLASH]) + G.add_edge(CELADON_CITY, 'Route 8', condition=None) + G.add_edge(CELADON_CITY, 'Route 16', condition=None) + G.add_edge('Route 16', 'Route 17', condition=None) + G.add_edge('Route 17', 'Route 18', condition=None) + G.add_edge('Route 18', FUCHSIA_CITY, condition=None) + G.add_edge(FUCHSIA_CITY,'Route 19', condition=None) + G.add_edge(FUCHSIA_CITY,'Route 15', condition=None) + G.add_edge('Route 19', 'Seafoam Islands', condition=[SURF]) + G.add_edge('Seafoam Islands', 'Route 20', condition=[SURF]) + G.add_edge('Route 20', CINNABAR_ISLAND, condition=[SURF]) + G.add_edge('Route 21', CINNABAR_ISLAND, condition=[SURF]) + G.add_edge('Route 22', VIRIDIAN_CITY, condition=None) + G.add_edge(VICTORY_ROAD, INDIGO_PLATEAU, condition=None) + G.add_edge('Route 12', 'Route 13', condition=None) + G.add_edge('Route 13', 'Route 14', condition=None) + G.add_edge('Route 14', 'Route 15', condition=None) + G.add_edge('Route 24', 'Route 25', condition=None) + G.add_edge('Route 10', POWER_PLANT, condition=[SURF]) + G.add_edge('Route 5', 'Underground Path', condition=None) + G.add_edge('Underground Path', 'Route 6', condition=None) + G.nodes[VERMILION_CITY]['grants_conditions'] = [ {'condition': THUNDER_BADGE, 'required_conditions': []}, {'condition': FLUTE_CHANNEL, 'required_conditions': [EXPN_CARD]}, @@ -383,4 +448,11 @@ def get_gold_silver_route() -> Graph: {'condition': VOLCANO_BADGE, 'required_conditions': []}, ] - return G \ No newline at end of file + return G + +def get_gold_silver_route() -> Graph: + + a = get_johto_route() + b = get_kanto_route() + + return a diff --git a/routes/Red_Blue_Route.py b/routes/Red_Blue_Route.py index af8a269..2e6aac0 100644 --- a/routes/Red_Blue_Route.py +++ b/routes/Red_Blue_Route.py @@ -1,6 +1,7 @@ import networkx as nx from networkx import Graph +from routes.game_world import RouteStage from routes.pokemon_game_desc import PokemonGameDesc PALLET_TOWN = 'Pallet Town' @@ -60,16 +61,23 @@ FLY_OUT_OF_BATTLE = 'Fly out of battle' def get_red_blue_desc() -> PokemonGameDesc: desc: PokemonGameDesc = PokemonGameDesc() - desc.graph = get_red_blue_route() - desc.game_name = "Red_Blue" + #desc.graph = get_red_blue_route() + desc.game_name = "Red" desc.towns_and_cities = [PALLET_TOWN, VIRIDIAN_CITY, PEWTER_CITY, CERULEAN_CITY, SAFFRON_CITY, CELADON_CITY, VERMILLION_CITY, FUCHSIA_CITY, CINNABAR_ISLAND] - desc.badges = [BOULDER_BADGE, CASCADE_BADGE, THUNDER_BADGE, RAINBOW_BADGE, MARSH_BADGE, SOUL_BADGE, VOLCANO_BADGE, EARTH_BADGE] desc.hms = [CUT, SURF, FLASH, STRENGTH, FLY] - desc.starting_town = PALLET_TOWN - desc.end_goal = CERULEAN_CAVE - desc.file_name = "red_blue_yellow_problem.pddl" desc.games_covered = ["Red", "Blue", "Yellow"] + + stage: RouteStage = RouteStage( + "Kanto", + get_red_blue_route(), + PALLET_TOWN, + CERULEAN_CAVE, + [BOULDER_BADGE, CASCADE_BADGE, THUNDER_BADGE, RAINBOW_BADGE, MARSH_BADGE, SOUL_BADGE, VOLCANO_BADGE, EARTH_BADGE], + set(), + [PALLET_TOWN, VIRIDIAN_CITY, PEWTER_CITY, CERULEAN_CITY, SAFFRON_CITY, CELADON_CITY, VERMILLION_CITY, FUCHSIA_CITY, CINNABAR_ISLAND] + ) + desc.stages.append(stage) return desc diff --git a/routes/game_world.py b/routes/game_world.py new file mode 100644 index 0000000..aaa6b50 --- /dev/null +++ b/routes/game_world.py @@ -0,0 +1,38 @@ +from typing import List, Set +import networkx as nx + +class GameWorld: + def __init__(self, graph: nx.Graph, start: str, end: str, goals: Set[str], must_visit: Set[str], initial_conditions: frozenset, towns_and_cities: Set[str]): + self.graph = graph + self.start = start + self.end = end + self.goals = goals + self.must_visit = must_visit + self.initial_conditions = initial_conditions + self.towns_and_cities = towns_and_cities + +class RouteStage: + def __init__(self, name: str, graph: nx.Graph, start: str, end: str, goals: Set[str], must_visit: Set[str], towns_and_cities: Set[str]): + self.name: str = name + self.graph: nx.Graph = graph # A function or class that returns a subgraph for this stage + self.start: str = start + self.end: str = end + self.goals: Set[str] = goals + self.must_visit: Set[str] = must_visit + self.towns_and_cities: Set[str] = towns_and_cities + + def create_world(self, previous_conditions=None): + # Combine initial_conditions with previous_conditions if needed + # previous_conditions are those acquired from the last stage. + #final_goals = self.goals.union(previous_conditions.get('badges', set())) if previous_conditions else self.goals + # Construct a GameWorld for this stage + world = GameWorld( + graph=self.graph, + start=self.start, + end=self.end, + goals=self.goals, + must_visit=self.must_visit, + initial_conditions=previous_conditions.get('conditions', frozenset()) if previous_conditions else frozenset(), + towns_and_cities = self.towns_and_cities + ) + return world \ No newline at end of file diff --git a/routes/pokemon_game_desc.py b/routes/pokemon_game_desc.py index 94e6ca5..767ff2d 100644 --- a/routes/pokemon_game_desc.py +++ b/routes/pokemon_game_desc.py @@ -1,154 +1,20 @@ -import heapq -import networkx as nx from typing import List, Set +import networkx as nx -FLY_OUT_OF_BATTLE = 'Fly out of battle' - -class State: - def __init__(self, location, conditions, cost, path, visited_required_nodes): - self.location = location - self.conditions = conditions # A frozenset of conditions - self.cost = cost - self.path = path # List of locations visited in order - self.visited_required_nodes = visited_required_nodes # A frozenset of required nodes visited - - def __lt__(self, other): - return self.cost < other.cost # For priority queue - -def heuristic(state, goal_conditions, required_nodes): - # Since we don't have actual distances, we can use the number of badges remaining as the heuristic - remaining_conditions = goal_conditions - state.conditions - remaining_nodes = required_nodes - state.visited_required_nodes - return len(remaining_conditions) + len(remaining_nodes) - -def is_goal_state(state, goal_location, goals, required_nodes): - return ( - state.location == goal_location and - goals.issubset(state.conditions) and - required_nodes.issubset(state.visited_required_nodes) - ) +from routes.game_world import RouteStage class PokemonGameDesc: def __init__(self): self.game_name: str = "" self.towns_and_cities: Set[str] = set() - self.badges: Set[str] = set() self.items: Set[str] = set() self.hms: Set[str] = set() - self.starting_town: str - self.end_goal: str self.flying_badge: str self.additional_goals: List[str] = [] self.one_way_routes: List[str] = [] self.must_visit: Set[str] = set() - self.graph: nx.Graph = nx.Graph() self.games_covered: List[str] = [] self.file_name: str = "" - - def astar_search(self): - from collections import deque - - self.goals = set(self.badges + self.additional_goals) - - # Priority queue for open states - open_list = [] - heapq.heappush(open_list, (0, State( - location=self.starting_town, - conditions=frozenset(), # Start with no conditions - cost=0, - path=[self.starting_town], - visited_required_nodes=frozenset([self.starting_town]) if self.starting_town in self.must_visit else frozenset() - ))) - - # Closed set to keep track of visited states - closed_set = {} - - while open_list: - _, current_state = heapq.heappop(open_list) - - # Check if we've reached the goal location with all required conditions - if is_goal_state(current_state, self.end_goal, self.goals, self.must_visit): - return current_state.path, current_state.cost, current_state.conditions - - # Check if we've already visited this state with equal or better conditions - state_key = (current_state.location, current_state.conditions, current_state.visited_required_nodes) - if state_key in closed_set and closed_set[state_key] <= current_state.cost: - continue # Skip this state - - closed_set[state_key] = current_state.cost - - # Expand neighbors via normal moves - for neighbor in self.graph.neighbors(current_state.location): - edge_data = self.graph.get_edge_data(current_state.location, neighbor) - edge_condition = edge_data.get('condition', []) - - if edge_condition is None: - edge_requires = set() - else: - edge_requires = set(edge_condition) - - # Check if we have the required conditions to traverse this edge - if not edge_requires.issubset(current_state.conditions): - continue # Can't traverse this edge - - # Update conditions based on grants at the neighbor node - neighbor_data = self.graph.nodes[neighbor] - new_conditions = set(current_state.conditions) - - # Check if the neighbor grants any conditions - grants = neighbor_data.get('grants_conditions', []) - for grant in grants: - required_for_grant = set(grant.get('required_conditions', [])) - if required_for_grant.issubset(new_conditions): - # We can acquire the condition - new_conditions.add(grant['condition']) - - # Update visited required nodes - new_visited_required_nodes = set(current_state.visited_required_nodes) - if neighbor in self.must_visit: - new_visited_required_nodes.add(neighbor) - - new_state = State( - location=neighbor, - conditions=frozenset(new_conditions), - cost=current_state.cost + 1, # Assuming uniform cost; adjust if needed - path=current_state.path + [neighbor], - visited_required_nodes=frozenset(new_visited_required_nodes) - ) - - estimated_total_cost = new_state.cost + heuristic(new_state, self.goals, self.must_visit) - - heapq.heappush(open_list, (estimated_total_cost, new_state)) - - # Expand neighbors via FLY if applicable - if FLY_OUT_OF_BATTLE in current_state.conditions and current_state.location in self.towns_and_cities: - for fly_target in self.towns_and_cities: - if fly_target != current_state.location and fly_target in current_state.path: - # You can fly to this location - new_conditions = set(current_state.conditions) - neighbor_data = self.graph.nodes[fly_target] - grants = neighbor_data.get('grants_conditions', []) - for grant in grants: - required_for_grant = set(grant.get('required_conditions', [])) - if required_for_grant.issubset(new_conditions): - new_conditions.add(grant['condition']) - - # Update visited required nodes - new_visited_required_nodes = set(current_state.visited_required_nodes) - if fly_target in self.must_visit: - new_visited_required_nodes.add(fly_target) - - fly_state = State( - location=fly_target, - conditions=frozenset(new_conditions), - cost=current_state.cost + 1, # Adjust cost if flying is different - path=current_state.path + [fly_target], - visited_required_nodes=frozenset(new_visited_required_nodes) - ) - estimated_total_cost = fly_state.cost + heuristic(fly_state, self.goals, self.must_visit) - heapq.heappush(open_list, (estimated_total_cost, fly_state)) - - return None # No path found - + self.stages: List[RouteStage] = [] __all__ = ["PokemonGameDesc"] diff --git a/routes/requirements.py b/routes/requirements.py new file mode 100644 index 0000000..fc3c8d5 --- /dev/null +++ b/routes/requirements.py @@ -0,0 +1,37 @@ +import json +from typing import Set + +def determine_must_visit_locations(plan_json: dict, db_conn, game_name: str) -> Set[str]: + # Extract needed Pokemon, find their encounter locations via db, return that set. + # No route logic here, just data derivation. + + target = None + for game in plan_json: + if game["game_name"] == game_name: + target = game + break + + if not target: + return set() + + # gather up all the pokemon needed for say crystal and find all the encounter routes/locations + needed_locations = set() + + game_info = db_conn.get_game_id_by_name(game_name) + + for key in target["pokemon"]: + catch_stats = target["pokemon"][key] + rep = catch_stats["representative"] + rand = db_conn.get_encounters(rep, "random") + static = db_conn.get_encounters(rep, "static") + encounters = [] + encounters.extend(rand) + encounters.extend(static) + + for encounter in encounters: + if encounter["game_id"] != game_info["id"]: + continue + + encounter_data = json.loads(encounter["data"]) + needed_locations.add(encounter_data["location"].replace("*", "")) + return needed_locations \ No newline at end of file diff --git a/routes/route_planner.py b/routes/route_planner.py new file mode 100644 index 0000000..6b754ef --- /dev/null +++ b/routes/route_planner.py @@ -0,0 +1,184 @@ + +from typing import List, Set +from routes.game_world import GameWorld +import heapq +import networkx as nx + +class RoutePlan: + def __init__(self, path: List[str], cost: int, conditions: Set[str]): + self.path = path + self.cost = cost + self.conditions = conditions + +FLY_OUT_OF_BATTLE = 'Fly out of battle' + +class State: + def __init__(self, location, conditions, cost, path, visited_required_nodes): + self.location = location + self.conditions = conditions # A frozenset of conditions + self.cost = cost + self.path = path # List of locations visited in order + self.visited_required_nodes = visited_required_nodes # A frozenset of required nodes visited + + def __lt__(self, other): + return self.cost < other.cost # For priority queue + +class RoutePlanner: + def __init__(self, world: GameWorld): + self.world: GameWorld = world + + def heuristic(self, state, goal_conditions, required_nodes): + # Since we don't have actual distances, we can use the number of badges remaining as the heuristic + remaining_conditions = goal_conditions - state.conditions + remaining_nodes = required_nodes - state.visited_required_nodes + return len(remaining_conditions) + len(remaining_nodes) + + def heuristic2(self, state, goal_conditions, end_goal, required_nodes, distances): + remaining_conditions = goal_conditions - state.conditions + remaining_nodes = required_nodes - state.visited_required_nodes + + # Find the shortest distance from current_state.location to any required node + eventually to the goal + # As a simple first step: take the minimum distance from the current node to any required node or the goal. + node_candidates = list(remaining_nodes) + [end_goal] + min_dist = float('inf') + for candidate in node_candidates: + d = distances.get((state.location, candidate), float('inf')) + if d < min_dist: + min_dist = d + + # If no must-visit nodes remain, just consider distance to the goal + if not remaining_nodes: + min_dist = distances.get((state.location, end_goal), float('inf')) + + # Combine with remaining conditions count as before + return len(remaining_conditions) + len(remaining_nodes) + (min_dist if min_dist != float('inf') else 0) + + + def is_goal_state(self, state, goal_location, goals, required_nodes): + return ( + state.location == goal_location and + goals.issubset(state.conditions) and + required_nodes.issubset(state.visited_required_nodes) + ) + + def compute_shortest_path(self, graph, key_nodes): + distances = {} # distances[(u,v)] = shortest distance from u to v ignoring conditions + + for node in key_nodes: + dist_from_node = nx.single_source_shortest_path_length(graph, node) + for other in key_nodes: + distances[(node, other)] = dist_from_node.get(other, float('inf')) + + return distances + + def astar_search(self) -> RoutePlan: + from collections import deque + + self.goals = set(self.world.goals) + + key_nodes = [self.world.start, self.world.end] + list(self.world.towns_and_cities) + if len(self.world.must_visit) > 0: + key_nodes += list(self.world.must_visit) + distances = self.compute_shortest_path(self.world.graph, key_nodes) + + # Priority queue for open states + open_list = [] + heapq.heappush(open_list, (0, State( + location=self.world.start, + conditions=self.world.initial_conditions, # Start with no conditions + cost=0, + path=[self.world.start], + visited_required_nodes=frozenset([self.world.start]) if self.world.start in self.world.must_visit else frozenset() + ))) + + # Closed set to keep track of visited states + closed_set = {} + + while open_list: + _, current_state = heapq.heappop(open_list) + + # Check if we've reached the goal location with all required conditions + if self.is_goal_state(current_state, self.world.end, self.goals, self.world.must_visit): + return RoutePlan(current_state.path, current_state.cost, current_state.conditions) + + # Check if we've already visited this state with equal or better conditions + state_key = (current_state.location, current_state.conditions, current_state.visited_required_nodes) + if state_key in closed_set and closed_set[state_key] <= current_state.cost: + continue # Skip this state + + closed_set[state_key] = current_state.cost + + # Expand neighbors via normal moves + for neighbor in self.world.graph.neighbors(current_state.location): + edge_data = self.world.graph.get_edge_data(current_state.location, neighbor) + edge_condition = edge_data.get('condition', []) + + if edge_condition is None: + edge_requires = set() + else: + edge_requires = set(edge_condition) + + # Check if we have the required conditions to traverse this edge + if not edge_requires.issubset(current_state.conditions): + continue # Can't traverse this edge + + # Update conditions based on grants at the neighbor node + neighbor_data = self.world.graph.nodes[neighbor] + new_conditions = set(current_state.conditions) + + # Check if the neighbor grants any conditions + grants = neighbor_data.get('grants_conditions', []) + for grant in grants: + required_for_grant = set(grant.get('required_conditions', [])) + if required_for_grant.issubset(new_conditions): + # We can acquire the condition + new_conditions.add(grant['condition']) + + # Update visited required nodes + new_visited_required_nodes = set(current_state.visited_required_nodes) + if neighbor in self.world.must_visit: + new_visited_required_nodes.add(neighbor) + + new_state = State( + location=neighbor, + conditions=frozenset(new_conditions), + cost=current_state.cost + 1, # Assuming uniform cost; adjust if needed + path=current_state.path + [neighbor], + visited_required_nodes=frozenset(new_visited_required_nodes) + ) + + #estimated_total_cost = new_state.cost + self.heuristic(new_state, self.goals, self.world.must_visit) + estimated_total_cost = new_state.cost + self.heuristic2(new_state, self.goals, self.world.end, self.world.must_visit, distances) + + heapq.heappush(open_list, (estimated_total_cost, new_state)) + + # Expand neighbors via FLY if applicable + if FLY_OUT_OF_BATTLE in current_state.conditions and current_state.location in self.world.towns_and_cities: + for fly_target in self.world.towns_and_cities: + if fly_target != current_state.location and fly_target in current_state.path: + # You can fly to this location + new_conditions = set(current_state.conditions) + neighbor_data = self.world.graph.nodes[fly_target] + grants = neighbor_data.get('grants_conditions', []) + for grant in grants: + required_for_grant = set(grant.get('required_conditions', [])) + if required_for_grant.issubset(new_conditions): + new_conditions.add(grant['condition']) + + # Update visited required nodes + new_visited_required_nodes = set(current_state.visited_required_nodes) + if fly_target in self.world.must_visit: + new_visited_required_nodes.add(fly_target) + + fly_state = State( + location=fly_target, + conditions=frozenset(new_conditions), + cost=current_state.cost + 1, # Adjust cost if flying is different + path=current_state.path + [fly_target], + visited_required_nodes=frozenset(new_visited_required_nodes) + ) + #estimated_total_cost = fly_state.cost + self.heuristic(fly_state, self.goals, self.world.must_visit) + estimated_total_cost = fly_state.cost + self.heuristic2(fly_state, self.goals, self.world.end, self.world.must_visit, distances) + heapq.heappush(open_list, (estimated_total_cost, fly_state)) + + return None # No path found \ No newline at end of file diff --git a/ui/main_window_controller.py b/ui/main_window_controller.py index 2425f57..aa08af1 100644 --- a/ui/main_window_controller.py +++ b/ui/main_window_controller.py @@ -14,6 +14,7 @@ from ui.workers.gather_evolutions_worker import GatherEvolutions from ui.workers.generate_PDDLs_worker import GeneratePDDLsWorker from ui.workers.generate_plan_worker import GeneratePlanWorker +from ui.workers.route_solve_worker import SolveRouteWorker from ui.workers.solve_pddl_route import SolvePDDLsWorker from utility.functions import get_display_name, sanitize_filename from db import db @@ -268,7 +269,8 @@ class MainWindowController: self.view.update_route_list(data) def solve_route(self, data: PokemonGameDesc): - worker = SolvePDDLsWorker(data) + #worker = SolvePDDLsWorker(data) + worker = SolveRouteWorker(data) worker.signals.finished.connect(self.on_route_solved) self.thread_pool.start(worker) diff --git a/ui/workers/generate_PDDLs_worker.py b/ui/workers/generate_PDDLs_worker.py index 8814a77..9a867a2 100644 --- a/ui/workers/generate_PDDLs_worker.py +++ b/ui/workers/generate_PDDLs_worker.py @@ -24,7 +24,7 @@ class GeneratePDDLsWorker(QRunnable): red_blue = get_red_blue_desc() gold_silver = get_gold_silver_desc() - generate_pddl_problem(red_blue) - generate_pddl_problem(gold_silver) + #generate_pddl_problem(red_blue) + #generate_pddl_problem(gold_silver) return [red_blue, gold_silver] \ No newline at end of file diff --git a/ui/workers/generate_plan_worker.py b/ui/workers/generate_plan_worker.py index 50e0618..659ef49 100644 --- a/ui/workers/generate_plan_worker.py +++ b/ui/workers/generate_plan_worker.py @@ -190,7 +190,7 @@ class GeneratePlanWorker(QRunnable): exclusive_groups = self.get_exclusive_groups(game['id']) pokemon_covered = set() - search_value = "0412-04-002-2" + search_value = "0025-01-000-1" if search_value in needed_pokemon: pass @@ -200,11 +200,14 @@ class GeneratePlanWorker(QRunnable): evolution_paths = {} evolution_predecessors = {} - all_pokemon = needed_pokemon | available_pokemon + temp_all_pokemon = needed_pokemon | available_pokemon for group in exclusive_groups: for pfic in group: - all_pokemon.add(pfic) + temp_all_pokemon.add(pfic) + + all_pokemon = list(temp_all_pokemon) + all_pokemon.sort(key=lambda x: parse_pfic(x)) for pfic in all_pokemon: chain = db.get_full_evolution_paths(pfic) @@ -237,7 +240,10 @@ class GeneratePlanWorker(QRunnable): if evolved_pfic in needed: container.add(evolved_pfic) - for pfic in needed_pokemon & available_pokemon: + pokemon_still_needed = list(needed_pokemon & available_pokemon) + pokemon_still_needed.sort(key=lambda x: parse_pfic(x)) + + for pfic in pokemon_still_needed: if pfic in pokemon_covered: continue previous_evolutions = evolution_predecessors.get(pfic, set()) @@ -342,7 +348,10 @@ class GeneratePlanWorker(QRunnable): missing_count = 0 - for pfic in required_pokemon: + required_pokemon_list = list(required_pokemon) + required_pokemon_list.sort(key=lambda x: parse_pfic(x)) + + for pfic in required_pokemon_list: pokemon_data = db.get_pokemon_details(pfic) evolution_chain = db.get_full_evolution_paths(pfic) if evolution_chain and not any(evolution_chain["predecessors"]): diff --git a/ui/workers/route_solve_worker.py b/ui/workers/route_solve_worker.py new file mode 100644 index 0000000..0db176c --- /dev/null +++ b/ui/workers/route_solve_worker.py @@ -0,0 +1,78 @@ +import json +from PyQt6.QtCore import QObject, pyqtSignal, QRunnable + +from db import db +from routes.game_world import GameWorld +from routes.pokemon_game_desc import PokemonGameDesc +from routes.requirements import determine_must_visit_locations +from routes.route_planner import RoutePlanner + +class SolveRouteWorkerSignals(QObject): + finished = pyqtSignal(list) + +class SolveRouteWorker(QRunnable): + def __init__(self, initial_data: PokemonGameDesc): + super().__init__() + self.signals = SolveRouteWorkerSignals() + self.initial_data = initial_data + + def run(self): + try: + stage_results = [] + current_conditions = { + 'badges': set(), + 'conditions': frozenset() + } + + for i, stage in enumerate(self.initial_data.stages): + if i == 0: + world = stage.create_world() + else: + prev_result = stage_results[-1] + current_conditions['conditions'] = prev_result.conditions + #current_conditions['badges'] = prev_result.conditions.intersection(all_badges_set) # if badges are a subset of conditions + stage.start = prev_result.path[-1] # end of last path is start of this stage + world = stage.create_world(previous_conditions=current_conditions) + + # First route calculation + planner = RoutePlanner(world) + initial_plan = planner.astar_search() + + # Load plan.json and determine if more visits are needed + with open("./temp/plan.json", 'r', encoding='utf-8') as f: + plan_json = json.load(f) + + needed_locations = determine_must_visit_locations(plan_json, db, self.initial_data.game_name) + missing_locations = needed_locations - set(initial_plan.path) + + # compare those to all the ones we visit in the generated route + + node_list = list(world.graph.nodes()) + + # add missing ones to the has_visited list and re-gen the problem, then run it again + nodes_to_visit = [] + for loc in missing_locations: + if loc in node_list: + nodes_to_visit.append(loc) + + print("Additional Nodes to Visit:", nodes_to_visit) + + result = None + if nodes_to_visit: + stage.must_visit = stage.must_visit.union(nodes_to_visit) + re_visisted = stage.create_world() + planner = RoutePlanner(re_visisted) + distances = planner.compute_shortest_path(re_visisted.graph, stage.must_visit) + result = planner.astar_search() + else: + result = initial_plan + + if result: + print(result.path) + + stage_results.append(result) + + self.signals.finished.emit(stage_results) + + except Exception as e: + print(f"Error: {e}") \ No newline at end of file diff --git a/ui/workers/solve_pddl_route.py b/ui/workers/solve_pddl_route.py index 229bdb8..48c03e7 100644 --- a/ui/workers/solve_pddl_route.py +++ b/ui/workers/solve_pddl_route.py @@ -7,8 +7,6 @@ import os from db import db from routes.pokemon_game_desc import PokemonGameDesc - -from downward.driver.main import main from utility.convert_to_pddl import format_name, generate_pddl_problem class SolvePDDLsWorkerSignals(QObject): @@ -86,7 +84,7 @@ class SolvePDDLsWorker(QRunnable): print("Additional Nodes to Visit:", nodes_to_visit) - data.must_visit = data.must_visit | set(nodes_to_visit) + data.must_visit = data.must_visit | set(nodes_to_visit[0:2]) route, _, _ = data.astar_search()