Add hierarchical route grouping for multi-area locations
Locations like Mt. Moon (with 1F, B1F, B2F floors) are now grouped so only one encounter can be logged per location group, enforcing Nuzlocke first-encounter rules correctly. - Add parent_route_id column with self-referential FK to routes table - Add parent/children relationships on Route model - Update games API to return hierarchical route structure - Add validation in encounters API to prevent parent route encounters and duplicate encounters within sibling routes (409 conflict) - Update frontend with collapsible RouteGroup component - Auto-derive route groups from PokeAPI location/location-area structure - Regenerate seed data with 70 parent routes and 315 child routes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -187,7 +187,11 @@ def aggregate_encounters(raw_encounters: list[dict]) -> list[dict]:
|
||||
|
||||
|
||||
def process_version(version_name: str, vg_info: dict) -> list[dict]:
|
||||
"""Process all locations for a specific game version."""
|
||||
"""Process all locations for a specific game version.
|
||||
|
||||
Creates hierarchical route structure where locations with multiple areas
|
||||
become parent routes with child routes for each area.
|
||||
"""
|
||||
print(f"\n--- Processing {version_name} ---")
|
||||
|
||||
region = load_resource("region", vg_info["region_id"])
|
||||
@@ -230,8 +234,38 @@ def process_version(version_name: str, vg_info: dict) -> list[dict]:
|
||||
else:
|
||||
all_encounters.extend(encounters)
|
||||
|
||||
# Area-specific encounters become separate routes
|
||||
if area_specific:
|
||||
# If we have multiple area-specific encounters, create a parent route
|
||||
# with child routes for each area (hierarchical grouping)
|
||||
if area_specific and len(area_specific) > 1:
|
||||
# Create parent route (no encounters - just a logical grouping)
|
||||
parent_order = order
|
||||
order += 1
|
||||
|
||||
child_routes = []
|
||||
for area_suffix, area_encs in area_specific.items():
|
||||
aggregated = aggregate_encounters(area_encs)
|
||||
if aggregated:
|
||||
route_name = f"{display_name} ({area_suffix})"
|
||||
for enc in aggregated:
|
||||
all_pokemon_dex.add(enc["national_dex"])
|
||||
child_routes.append({
|
||||
"name": route_name,
|
||||
"order": order,
|
||||
"encounters": aggregated,
|
||||
})
|
||||
order += 1
|
||||
|
||||
# Only add parent if we have child routes
|
||||
if child_routes:
|
||||
routes.append({
|
||||
"name": display_name,
|
||||
"order": parent_order,
|
||||
"encounters": [], # Parent routes have no encounters
|
||||
"children": child_routes,
|
||||
})
|
||||
|
||||
elif area_specific:
|
||||
# Only one area-specific route - don't create parent/child, just use suffix
|
||||
for area_suffix, area_encs in area_specific.items():
|
||||
aggregated = aggregate_encounters(area_encs)
|
||||
if aggregated:
|
||||
@@ -245,6 +279,7 @@ def process_version(version_name: str, vg_info: dict) -> list[dict]:
|
||||
})
|
||||
order += 1
|
||||
|
||||
# Non-area-specific encounters (or single area without suffix)
|
||||
if all_encounters:
|
||||
aggregated = aggregate_encounters(all_encounters)
|
||||
if aggregated:
|
||||
@@ -257,8 +292,13 @@ def process_version(version_name: str, vg_info: dict) -> list[dict]:
|
||||
})
|
||||
order += 1
|
||||
|
||||
print(f" Routes with encounters: {len(routes)}")
|
||||
total_enc = sum(len(r["encounters"]) for r in routes)
|
||||
# Count routes including children
|
||||
total_routes = sum(1 + len(r.get("children", [])) for r in routes)
|
||||
print(f" Routes with encounters: {total_routes}")
|
||||
total_enc = sum(
|
||||
len(r["encounters"]) + sum(len(c["encounters"]) for c in r.get("children", []))
|
||||
for r in routes
|
||||
)
|
||||
print(f" Total encounter entries: {total_enc}")
|
||||
|
||||
return routes
|
||||
|
||||
@@ -66,20 +66,54 @@ async def upsert_routes(
|
||||
game_id: int,
|
||||
routes: list[dict],
|
||||
) -> dict[str, int]:
|
||||
"""Upsert route records for a game, return {name: id} mapping."""
|
||||
"""Upsert route records for a game, return {name: id} mapping.
|
||||
|
||||
Handles hierarchical routes: routes with 'children' are parent routes,
|
||||
and their children get parent_route_id set accordingly.
|
||||
"""
|
||||
# First pass: upsert all parent routes (without parent_route_id)
|
||||
for route in routes:
|
||||
stmt = insert(Route).values(
|
||||
name=route["name"],
|
||||
game_id=game_id,
|
||||
order=route["order"],
|
||||
parent_route_id=None, # Parent routes have no parent
|
||||
).on_conflict_do_update(
|
||||
constraint="uq_routes_game_name",
|
||||
set_={"order": route["order"]},
|
||||
set_={"order": route["order"], "parent_route_id": None},
|
||||
)
|
||||
await session.execute(stmt)
|
||||
|
||||
await session.flush()
|
||||
|
||||
# Get mapping of parent routes
|
||||
result = await session.execute(
|
||||
select(Route.name, Route.id).where(Route.game_id == game_id)
|
||||
)
|
||||
name_to_id = {row.name: row.id for row in result}
|
||||
|
||||
# Second pass: upsert child routes with parent_route_id
|
||||
for route in routes:
|
||||
children = route.get("children", [])
|
||||
if not children:
|
||||
continue
|
||||
|
||||
parent_id = name_to_id[route["name"]]
|
||||
for child in children:
|
||||
stmt = insert(Route).values(
|
||||
name=child["name"],
|
||||
game_id=game_id,
|
||||
order=child["order"],
|
||||
parent_route_id=parent_id,
|
||||
).on_conflict_do_update(
|
||||
constraint="uq_routes_game_name",
|
||||
set_={"order": child["order"], "parent_route_id": parent_id},
|
||||
)
|
||||
await session.execute(stmt)
|
||||
|
||||
await session.flush()
|
||||
|
||||
# Return full mapping including children
|
||||
result = await session.execute(
|
||||
select(Route.name, Route.id).where(Route.game_id == game_id)
|
||||
)
|
||||
|
||||
@@ -66,10 +66,24 @@ async def seed():
|
||||
print(f" Warning: route '{route['name']}' not found")
|
||||
continue
|
||||
|
||||
enc_count = await upsert_route_encounters(
|
||||
session, route_id, route["encounters"], dex_to_id
|
||||
)
|
||||
total_encounters += enc_count
|
||||
# Parent routes may have empty encounters
|
||||
if route["encounters"]:
|
||||
enc_count = await upsert_route_encounters(
|
||||
session, route_id, route["encounters"], dex_to_id
|
||||
)
|
||||
total_encounters += enc_count
|
||||
|
||||
# Handle child routes
|
||||
for child in route.get("children", []):
|
||||
child_id = route_map.get(child["name"])
|
||||
if child_id is None:
|
||||
print(f" Warning: child route '{child['name']}' not found")
|
||||
continue
|
||||
|
||||
enc_count = await upsert_route_encounters(
|
||||
session, child_id, child["encounters"], dex_to_id
|
||||
)
|
||||
total_encounters += enc_count
|
||||
|
||||
print(f" {game_slug}: {len(route_map)} routes")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user