Sort seed data routes by game progression instead of alphabetically
Add ROUTE_ORDER maps for Kanto (FRLG), Johto (HGSS), and Hoenn (Emerald) progressions with aliases for related version groups. Add Export Order button to admin game detail page for iterating on route orderings. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
---
|
||||
# nuzlocke-tracker-37gk
|
||||
title: Curate route ordering to match game progression
|
||||
status: completed
|
||||
type: feature
|
||||
priority: normal
|
||||
created_at: 2026-02-07T12:25:05Z
|
||||
updated_at: 2026-02-07T12:27:11Z
|
||||
---
|
||||
|
||||
Implement ROUTE_ORDER in fetch_pokeapi.py for progression-based sorting, and add Export Order button to AdminGameDetail. See bean j28y for full plan.
|
||||
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
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
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
@@ -449,6 +449,260 @@ INCLUDED_METHODS = {
|
||||
"headbutt",
|
||||
}
|
||||
|
||||
# Route progression order by version group.
|
||||
# Keys are version group names from VERSION_GROUPS. Names must match
|
||||
# clean_location_name() output. Routes not listed fall to the end alphabetically.
|
||||
ROUTE_ORDER: dict[str, list[str]] = {
|
||||
"firered-leafgreen": [
|
||||
# Main Kanto progression
|
||||
"Pallet Town",
|
||||
"Route 1",
|
||||
"Viridian City",
|
||||
"Route 22",
|
||||
"Route 2",
|
||||
"Viridian Forest",
|
||||
"Route 3",
|
||||
"Mt Moon",
|
||||
"Route 4",
|
||||
"Cerulean City",
|
||||
"Route 24",
|
||||
"Route 25",
|
||||
"Route 5",
|
||||
"Route 6",
|
||||
"Vermilion City",
|
||||
"Ss Anne",
|
||||
"Route 11",
|
||||
"Digletts Cave",
|
||||
"Route 9",
|
||||
"Route 10",
|
||||
"Rock Tunnel",
|
||||
"Power Plant",
|
||||
"Route 8",
|
||||
"Route 7",
|
||||
"Celadon City",
|
||||
"Route 16",
|
||||
"Route 17",
|
||||
"Route 18",
|
||||
"Fuchsia City",
|
||||
"Safari Zone",
|
||||
"Route 15",
|
||||
"Route 14",
|
||||
"Route 13",
|
||||
"Route 12",
|
||||
"Pokemon Tower",
|
||||
"Sea Route 19",
|
||||
"Sea Route 20",
|
||||
"Seafoam Islands",
|
||||
"Cinnabar Island",
|
||||
"Pokemon Mansion",
|
||||
"Sea Route 21",
|
||||
"Route 23",
|
||||
"Victory Road 2",
|
||||
"Cerulean Cave",
|
||||
# Sevii Islands
|
||||
"One Island",
|
||||
"Kindle Road",
|
||||
"Treasure Beach",
|
||||
"Mt Ember",
|
||||
"Cape Brink",
|
||||
"Berry Forest",
|
||||
"Bond Bridge",
|
||||
"Three Isle Port",
|
||||
"Four Island",
|
||||
"Icefall Cave",
|
||||
"Resort Gorgeous",
|
||||
"Water Labyrinth",
|
||||
"Five Island",
|
||||
"Five Isle Meadow",
|
||||
"Memorial Pillar",
|
||||
"Outcast Island",
|
||||
"Green Path",
|
||||
"Water Path",
|
||||
"Ruin Valley",
|
||||
"Lost Cave",
|
||||
"Pattern Bush",
|
||||
"Trainer Tower",
|
||||
"Canyon Entrance",
|
||||
"Sevault Canyon",
|
||||
"Tanoby Ruins",
|
||||
"Monean Chamber",
|
||||
"Liptoo Chamber",
|
||||
"Weepth Chamber",
|
||||
"Dilford Chamber",
|
||||
"Scufib Chamber",
|
||||
"Rixy Chamber",
|
||||
"Viapos Chamber",
|
||||
"Altering Cave",
|
||||
],
|
||||
"heartgold-soulsilver": [
|
||||
# Johto
|
||||
"New Bark Town",
|
||||
"Route 29",
|
||||
"Cherrygrove City",
|
||||
"Route 30",
|
||||
"Route 31",
|
||||
"Dark Cave",
|
||||
"Violet City",
|
||||
"Sprout Tower",
|
||||
"Route 32",
|
||||
"Ruins Of Alph",
|
||||
"Union Cave",
|
||||
"Route 33",
|
||||
"Slowpoke Well",
|
||||
"Route 34",
|
||||
"Ilex Forest",
|
||||
"National Park",
|
||||
"Route 35",
|
||||
"Route 36",
|
||||
"Route 37",
|
||||
"Ecruteak City",
|
||||
"Burned Tower",
|
||||
"Bell Tower",
|
||||
"Route 38",
|
||||
"Route 39",
|
||||
"Olivine City",
|
||||
"Sea Route 40",
|
||||
"Sea Route 41",
|
||||
"Cianwood City",
|
||||
"Route 42",
|
||||
"Mt Mortar",
|
||||
"Lake Of Rage",
|
||||
"Route 43",
|
||||
"Route 44",
|
||||
"Ice Path",
|
||||
"Blackthorn City",
|
||||
"Dragons Den",
|
||||
"Route 45",
|
||||
"Route 46",
|
||||
"Route 47",
|
||||
"Route 48",
|
||||
"Safari Zone",
|
||||
"Whirl Islands",
|
||||
"Tohjo Falls",
|
||||
# Kanto post-game
|
||||
"Route 27",
|
||||
"Route 26",
|
||||
"Victory Road 1",
|
||||
"Route 28",
|
||||
"Mt Silver",
|
||||
"Pallet Town",
|
||||
"Route 1",
|
||||
"Viridian City",
|
||||
"Viridian Forest",
|
||||
"Route 2",
|
||||
"Route 3",
|
||||
"Mt Moon",
|
||||
"Route 4",
|
||||
"Cerulean City",
|
||||
"Route 24",
|
||||
"Route 25",
|
||||
"Cerulean Cave",
|
||||
"Route 5",
|
||||
"Route 6",
|
||||
"Vermilion City",
|
||||
"Route 7",
|
||||
"Route 8",
|
||||
"Route 9",
|
||||
"Route 10",
|
||||
"Rock Tunnel",
|
||||
"Route 11",
|
||||
"Digletts Cave",
|
||||
"Route 12",
|
||||
"Route 13",
|
||||
"Route 14",
|
||||
"Route 15",
|
||||
"Fuchsia City",
|
||||
"Route 16",
|
||||
"Route 17",
|
||||
"Route 18",
|
||||
"Celadon City",
|
||||
"Sea Route 19",
|
||||
"Sea Route 20",
|
||||
"Seafoam Islands",
|
||||
"Cinnabar Island",
|
||||
"Sea Route 21",
|
||||
"Route 22",
|
||||
# Misc
|
||||
"Unknown All Poliwag",
|
||||
"Unknown All Rattata",
|
||||
"Unknown All Bugs",
|
||||
],
|
||||
"emerald": [
|
||||
"Route 101",
|
||||
"Oldale Town",
|
||||
"Route 103",
|
||||
"Route 102",
|
||||
"Petalburg City",
|
||||
"Route 104",
|
||||
"Petalburg Woods",
|
||||
"Rusturf Tunnel",
|
||||
"Route 116",
|
||||
"Route 105",
|
||||
"Route 106",
|
||||
"Dewford Town",
|
||||
"Granite Cave",
|
||||
"Route 107",
|
||||
"Route 108",
|
||||
"Route 109",
|
||||
"Slateport City",
|
||||
"Route 110",
|
||||
"New Mauville",
|
||||
"Route 117",
|
||||
"Route 111",
|
||||
"Mirage Tower",
|
||||
"Route 112",
|
||||
"Fiery Path",
|
||||
"Jagged Pass",
|
||||
"Route 113",
|
||||
"Route 114",
|
||||
"Meteor Falls",
|
||||
"Route 115",
|
||||
"Route 118",
|
||||
"Route 119",
|
||||
"Route 120",
|
||||
"Route 121",
|
||||
"Safari Zone",
|
||||
"Lilycove City",
|
||||
"Route 122",
|
||||
"Mt Pyre",
|
||||
"Route 123",
|
||||
"Magma Hideout",
|
||||
"Route 124",
|
||||
"Mossdeep City",
|
||||
"Route 125",
|
||||
"Shoal Cave",
|
||||
"Route 126",
|
||||
"Sootopolis City",
|
||||
"Cave Of Origin",
|
||||
"Route 127",
|
||||
"Route 128",
|
||||
"Seafloor Cavern",
|
||||
"Route 129",
|
||||
"Route 130",
|
||||
"Route 131",
|
||||
"Pacifidlog Town",
|
||||
"Route 132",
|
||||
"Route 133",
|
||||
"Route 134",
|
||||
"Ever Grande City",
|
||||
"Victory Road",
|
||||
"Sky Pillar",
|
||||
"Abandoned Ship",
|
||||
"Desert Underpass",
|
||||
"Artisan Cave",
|
||||
"Altering Cave",
|
||||
],
|
||||
}
|
||||
|
||||
# Aliases — version groups sharing same route progression
|
||||
ROUTE_ORDER["red-blue"] = ROUTE_ORDER["firered-leafgreen"]
|
||||
ROUTE_ORDER["yellow"] = ROUTE_ORDER["firered-leafgreen"]
|
||||
ROUTE_ORDER["lets-go"] = ROUTE_ORDER["firered-leafgreen"]
|
||||
ROUTE_ORDER["gold-silver"] = ROUTE_ORDER["heartgold-soulsilver"]
|
||||
ROUTE_ORDER["crystal"] = ROUTE_ORDER["heartgold-soulsilver"]
|
||||
ROUTE_ORDER["ruby-sapphire"] = ROUTE_ORDER["emerald"]
|
||||
ROUTE_ORDER["omega-ruby-alpha-sapphire"] = ROUTE_ORDER["emerald"]
|
||||
|
||||
# Collect all pokemon dex numbers across games
|
||||
all_pokemon_dex: set[int] = set()
|
||||
|
||||
@@ -482,6 +736,26 @@ def clean_area_name(area_name: str, location_name: str) -> str | None:
|
||||
return area_name.replace("-", " ").title()
|
||||
|
||||
|
||||
def sort_routes_by_progression(routes: list[dict], vg_key: str) -> list[dict]:
|
||||
"""Sort routes by game progression order for the given version group.
|
||||
|
||||
Routes not in the ordering list fall to the end, sorted alphabetically.
|
||||
Uses starts-with matching so 'Route 2' matches 'Route 2 (South Towards ...)'.
|
||||
"""
|
||||
order_list = ROUTE_ORDER.get(vg_key)
|
||||
if not order_list:
|
||||
return routes
|
||||
|
||||
def sort_key(route: dict) -> tuple[int, str]:
|
||||
name = route["name"]
|
||||
for i, ordered_name in enumerate(order_list):
|
||||
if name == ordered_name or name.startswith(ordered_name + " ("):
|
||||
return (i, name)
|
||||
return (len(order_list), name) # Unmatched → end, alphabetical
|
||||
|
||||
return sorted(routes, key=sort_key)
|
||||
|
||||
|
||||
def get_encounters_for_area(area_id: int, version_name: str) -> list[dict]:
|
||||
"""Get encounter data for a location area, filtered by version."""
|
||||
area = load_resource("location-area", area_id)
|
||||
@@ -539,7 +813,7 @@ def aggregate_encounters(raw_encounters: list[dict]) -> list[dict]:
|
||||
return sorted(result, key=lambda x: (-x["encounter_rate"], x["pokemon_name"]))
|
||||
|
||||
|
||||
def process_version(version_name: str, vg_info: dict) -> list[dict]:
|
||||
def process_version(version_name: str, vg_info: dict, vg_key: str) -> list[dict]:
|
||||
"""Process all locations for a specific game version.
|
||||
|
||||
Creates hierarchical route structure where locations with multiple areas
|
||||
@@ -558,7 +832,6 @@ def process_version(version_name: str, vg_info: dict) -> list[dict]:
|
||||
print(f" Found {len(location_refs)} locations")
|
||||
|
||||
routes = []
|
||||
order = 1
|
||||
|
||||
for loc_ref in location_refs:
|
||||
loc_name = loc_ref["name"]
|
||||
@@ -590,10 +863,6 @@ def process_version(version_name: str, vg_info: dict) -> list[dict]:
|
||||
# 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)
|
||||
@@ -603,16 +872,15 @@ def process_version(version_name: str, vg_info: dict) -> list[dict]:
|
||||
all_pokemon_dex.add(enc["national_dex"])
|
||||
child_routes.append({
|
||||
"name": route_name,
|
||||
"order": order,
|
||||
"order": 0,
|
||||
"encounters": aggregated,
|
||||
})
|
||||
order += 1
|
||||
|
||||
# Only add parent if we have child routes
|
||||
if child_routes:
|
||||
routes.append({
|
||||
"name": display_name,
|
||||
"order": parent_order,
|
||||
"order": 0,
|
||||
"encounters": [], # Parent routes have no encounters
|
||||
"children": child_routes,
|
||||
})
|
||||
@@ -627,10 +895,9 @@ def process_version(version_name: str, vg_info: dict) -> list[dict]:
|
||||
all_pokemon_dex.add(enc["national_dex"])
|
||||
routes.append({
|
||||
"name": route_name,
|
||||
"order": order,
|
||||
"order": 0,
|
||||
"encounters": aggregated,
|
||||
})
|
||||
order += 1
|
||||
|
||||
# Non-area-specific encounters (or single area without suffix)
|
||||
if all_encounters:
|
||||
@@ -640,10 +907,21 @@ def process_version(version_name: str, vg_info: dict) -> list[dict]:
|
||||
all_pokemon_dex.add(enc["national_dex"])
|
||||
routes.append({
|
||||
"name": display_name,
|
||||
"order": order,
|
||||
"order": 0,
|
||||
"encounters": aggregated,
|
||||
})
|
||||
order += 1
|
||||
|
||||
# Sort routes by game progression order
|
||||
routes = sort_routes_by_progression(routes, vg_key)
|
||||
|
||||
# Reassign sequential order values
|
||||
order = 1
|
||||
for route in routes:
|
||||
route["order"] = order
|
||||
order += 1
|
||||
for child in route.get("children", []):
|
||||
child["order"] = order
|
||||
order += 1
|
||||
|
||||
# Count routes including children
|
||||
total_routes = sum(1 + len(r.get("children", [])) for r in routes)
|
||||
@@ -870,9 +1148,9 @@ def main():
|
||||
print(f"Wrote {len(games)} games to games.json")
|
||||
|
||||
# Process each version
|
||||
for vg_info in VERSION_GROUPS.values():
|
||||
for vg_key, vg_info in VERSION_GROUPS.items():
|
||||
for ver_name in vg_info["versions"]:
|
||||
routes = process_version(ver_name, vg_info)
|
||||
routes = process_version(ver_name, vg_info, vg_key)
|
||||
write_json(f"{ver_name}.json", routes)
|
||||
|
||||
# Fetch all Pokemon species
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom'
|
||||
import {
|
||||
DndContext,
|
||||
@@ -158,12 +159,24 @@ export function AdminGameDetail() {
|
||||
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-medium">Routes ({routes.length})</h3>
|
||||
<button
|
||||
onClick={() => setShowCreate(true)}
|
||||
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700"
|
||||
>
|
||||
Add Route
|
||||
</button>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
const names = routes.map((r) => r.name)
|
||||
navigator.clipboard.writeText(JSON.stringify(names, null, 2))
|
||||
toast.success('Route order copied to clipboard')
|
||||
}}
|
||||
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
>
|
||||
Export Order
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowCreate(true)}
|
||||
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700"
|
||||
>
|
||||
Add Route
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{routes.length === 0 ? (
|
||||
|
||||
Reference in New Issue
Block a user