Merge branch 'develop' into feature/add-boss-data
Some checks failed
CI / backend-lint (pull_request) Failing after 8s
CI / frontend-lint (pull_request) Successful in 31s

This commit is contained in:
2026-02-11 21:52:41 +01:00
21 changed files with 833 additions and 49 deletions

View File

@@ -0,0 +1,29 @@
"""add naming_scheme to nuzlocke_runs
Revision ID: e5f6a7b8c9d1
Revises: d4e5f6a7b9c0
Create Date: 2026-02-11 12:00:00.000000
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "e5f6a7b8c9d1"
down_revision: str | Sequence[str] | None = "d4e5f6a7b9c0"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.add_column(
"nuzlocke_runs",
sa.Column("naming_scheme", sa.String(50), nullable=True),
)
def downgrade() -> None:
op.drop_column("nuzlocke_runs", "naming_scheme")

View File

@@ -0,0 +1,28 @@
"""merge naming_scheme and genlocke_transfers
Revision ID: e5f70a1ca323
Revises: e5f6a7b8c9d1, e5f6a7b9c0d1
Create Date: 2026-02-11 21:49:29.942841
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'e5f70a1ca323'
down_revision: Union[str, Sequence[str], None] = ('e5f6a7b8c9d1', 'e5f6a7b9c0d1')
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
pass
def downgrade() -> None:
"""Downgrade schema."""
pass

View File

@@ -19,10 +19,41 @@ from app.schemas.run import (
RunResponse,
RunUpdate,
)
from app.services.naming import get_naming_categories, suggest_names
router = APIRouter()
@router.get("/naming-categories", response_model=list[str])
async def list_naming_categories():
return get_naming_categories()
@router.get("/{run_id}/name-suggestions", response_model=list[str])
async def get_name_suggestions(
run_id: int,
count: int = 10,
session: AsyncSession = Depends(get_session),
):
run = await session.get(NuzlockeRun, run_id)
if run is None:
raise HTTPException(status_code=404, detail="Run not found")
if not run.naming_scheme:
return []
# Collect nicknames already used in this run
result = await session.execute(
select(Encounter.nickname).where(
Encounter.run_id == run_id,
Encounter.nickname.isnot(None),
)
)
used_names = {row[0] for row in result}
return suggest_names(run.naming_scheme, used_names, count)
@router.post("", response_model=RunResponse, status_code=201)
async def create_run(data: RunCreate, session: AsyncSession = Depends(get_session)):
# Validate game exists
@@ -35,6 +66,7 @@ async def create_run(data: RunCreate, session: AsyncSession = Depends(get_sessio
name=data.name,
status="active",
rules=data.rules,
naming_scheme=data.naming_scheme,
)
session.add(run)
await session.commit()

View File

@@ -22,6 +22,7 @@ class NuzlockeRun(Base):
)
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
hof_encounter_ids: Mapped[list[int] | None] = mapped_column(JSONB, default=None)
naming_scheme: Mapped[str | None] = mapped_column(String(50), nullable=True)
game: Mapped["Game"] = relationship(back_populates="runs")
encounters: Mapped[list["Encounter"]] = relationship(back_populates="run")

View File

@@ -9,6 +9,7 @@ class RunCreate(CamelModel):
game_id: int
name: str
rules: dict = {}
naming_scheme: str | None = None
class RunUpdate(CamelModel):
@@ -16,6 +17,7 @@ class RunUpdate(CamelModel):
status: str | None = None
rules: dict | None = None
hof_encounter_ids: list[int] | None = None
naming_scheme: str | None = None
class RunResponse(CamelModel):
@@ -25,6 +27,7 @@ class RunResponse(CamelModel):
status: str
rules: dict
hof_encounter_ids: list[int] | None = None
naming_scheme: str | None = None
started_at: datetime
completed_at: datetime | None

View File

@@ -0,0 +1,372 @@
{
"mythology": [
"Zeus", "Hera", "Apollo", "Athena", "Ares",
"Hermes", "Artemis", "Hades", "Poseidon", "Demeter",
"Hephaestus", "Aphrodite", "Dionysus", "Persephone", "Eros",
"Nike", "Helios", "Selene", "Nyx", "Chronos",
"Atlas", "Titan", "Hydra", "Cerberus", "Minotaur",
"Medusa", "Pegasus", "Phoenix", "Chimera", "Cyclops",
"Icarus", "Orpheus", "Perseus", "Achilles", "Odysseus",
"Ajax", "Hector", "Paris", "Helen", "Circe",
"Calypso", "Siren", "Muse", "Oracle", "Sphinx",
"Styx", "Elysium", "Olympus", "Tartarus", "Nemesis",
"Odin", "Thor", "Loki", "Freya", "Tyr",
"Baldur", "Fenrir", "Jormungandr", "Ragnarok", "Valkyrie",
"Mjolnir", "Yggdrasil", "Bifrost", "Valhalla", "Asgard",
"Midgard", "Norns", "Huginn", "Muninn", "Sleipnir",
"Ra", "Osiris", "Isis", "Anubis", "Thoth",
"Bastet", "Sekhmet", "Sobek", "Horus", "Seth",
"Nephthys", "Ammit", "Scarab", "Apophis", "Khepri",
"Amaterasu", "Susanoo", "Tsukuyomi", "Izanagi", "Izanami",
"Raijin", "Fujin", "Inari", "Kitsune", "Tengu",
"Oni", "Tanuki", "Yokai", "Kami", "Byakko",
"Vishnu", "Shiva", "Brahma", "Kali", "Ganesh",
"Lakshmi", "Hanuman", "Indra", "Agni", "Surya",
"Garuda", "Naga", "Deva", "Asura", "Dharma",
"Karma", "Mantra", "Lotus", "Chakra", "Maya",
"Brigid", "Morrigan", "Dagda", "Lugh", "Danu",
"Cernunnos", "Oberon", "Titania", "Merlin", "Avalon",
"Excalibur", "Druid", "Banshee", "Sidhe", "Fomorian",
"Leviathan", "Golem", "Djinn", "Ifrit", "Typhon",
"Griffin", "Kraken", "Wyrm", "Drake", "Wyvern",
"Basilisk", "Manticore", "Harpy", "Gorgon", "Triton",
"Prometheus", "Pandora", "Daphne", "Echo", "Narcissus",
"Midas", "Theseus", "Orion", "Castor", "Pollux",
"Zephyr", "Boreas", "Aether", "Chaos", "Gaia",
"Uranus", "Rhea", "Hyperion", "Themis", "Mnemosyne",
"Clio", "Fury", "Fate", "Moira", "Clotho"
],
"food": [
"Basil", "Sage", "Thyme", "Rosemary", "Saffron",
"Ginger", "Pepper", "Cinnamon", "Nutmeg", "Clove",
"Paprika", "Wasabi", "Cumin", "Fennel", "Dill",
"Anise", "Cardamom", "Turmeric", "Coriander", "Oregano",
"Tarragon", "Chive", "Parsley", "Mint", "Vanilla",
"Mango", "Kiwi", "Peach", "Cherry", "Plum",
"Fig", "Lemon", "Lime", "Melon", "Berry",
"Olive", "Cocoa", "Mocha", "Latte", "Chai",
"Matcha", "Espresso", "Cider", "Mead", "Porter",
"Stout", "Sake", "Roux", "Bisque", "Broth",
"Frappe", "Sorbet", "Gelato", "Truffle", "Praline",
"Nougat", "Toffee", "Caramel", "Fudge", "Brioche",
"Croissant", "Baguette", "Pretzel", "Dumpling", "Gyoza",
"Mochi", "Tempura", "Ramen", "Udon", "Soba",
"Pesto", "Salsa", "Kimchi", "Naan", "Tofu",
"Panko", "Miso", "Tahini", "Hummus", "Falafel",
"Burrito", "Taco", "Nacho", "Churro", "Crepe",
"Waffle", "Scone", "Crumpet", "Biscuit", "Cobbler",
"Crisp", "Tart", "Torte", "Mousse", "Souffle",
"Quiche", "Gratin", "Fondue", "Risotto", "Gnocchi",
"Ravioli", "Penne", "Orzo", "Pilaf", "Paella",
"Tagine", "Curry", "Satay", "Bulgogi", "Teriyaki",
"Ponzu", "Sriracha", "Harissa", "Chutney", "Relish",
"Pickle", "Caper", "Anchovy", "Prawn", "Scallop",
"Brisket", "Bison", "Quinoa", "Barley", "Millet",
"Almond", "Pecan", "Walnut", "Cashew", "Pistachio",
"Hazel", "Acai", "Guava", "Lychee", "Papaya",
"Tamarind", "Kumquat", "Clementine", "Nectar", "Honey",
"Maple", "Agave", "Molasses", "Butter", "Cream",
"Brie", "Gouda", "Cheddar", "Ricotta", "Paneer",
"Toasted", "Smoked", "Braised", "Seared", "Glazed",
"Roasted", "Grilled", "Poached", "Zest", "Tang",
"Crunch", "Morsel", "Nibble", "Drizzle", "Dash",
"Pinch", "Sprig", "Garnish", "Feast", "Brunch",
"Savory", "Umami", "Tangy", "Citrus", "Jambon"
],
"space": [
"Nova", "Nebula", "Quasar", "Pulsar", "Cosmos",
"Comet", "Meteor", "Astral", "Stellar", "Solar",
"Lunar", "Orbit", "Eclipse", "Galaxy", "Vortex",
"Zenith", "Nadir", "Apogee", "Perigee", "Epoch",
"Apollo", "Gemini", "Mercury", "Venus", "Mars",
"Jupiter", "Saturn", "Neptune", "Uranus", "Pluto",
"Titan", "Europa", "Io", "Callisto", "Ganymede",
"Triton", "Charon", "Phobos", "Deimos", "Ceres",
"Juno", "Vesta", "Pallas", "Eris", "Sedna",
"Sirius", "Rigel", "Vega", "Altair", "Deneb",
"Polaris", "Canopus", "Betelgeuse", "Antares", "Aldebaran",
"Spica", "Arcturus", "Capella", "Procyon", "Achernar",
"Castor", "Pollux", "Regulus", "Fomalhaut", "Mira",
"Bellatrix", "Mintaka", "Alnilam", "Alnitak", "Saiph",
"Orion", "Lyra", "Draco", "Hydra", "Corvus",
"Aquila", "Cygnus", "Phoenix", "Andromeda", "Cassiopeia",
"Perseus", "Centauri", "Pegasus", "Scorpius", "Leo",
"Aries", "Virgo", "Libra", "Pisces", "Taurus",
"Photon", "Neutron", "Proton", "Plasma", "Flux",
"Prism", "Spectrum", "Horizon", "Corona", "Halo",
"Aurora", "Solstice", "Equinox", "Parallax", "Parsec",
"Lightyear", "Warp", "Void", "Rift", "Abyss",
"Singularity", "Binary", "Cluster", "Remnant", "Dwarf",
"Giant", "Supernova", "Stardust", "Starfall", "Skyfire",
"Sunspot", "Flare", "Radiance", "Luminous", "Beacon",
"Voyager", "Pioneer", "Sputnik", "Hubble", "Kepler",
"Galileo", "Cassini", "Rosetta", "Artemis", "Chandra",
"Solaris", "Nebular", "Asteria", "Celestia", "Astra",
"Solis", "Lumen", "Ignis", "Ember", "Blaze",
"Spark", "Glimmer", "Shimmer", "Twilight", "Dusk",
"Dawn", "Crescent", "Waning", "Waxing", "Umbra",
"Penumbra", "Perihelion", "Aphelion", "Azimuth", "Meridian",
"Helix", "Spiral", "Ring", "Arc", "Bolt",
"Surge", "Pulse", "Wave", "Drift", "Phase",
"Xenon", "Neon", "Argon", "Helium", "Lithium"
],
"nature": [
"Willow", "Cedar", "Birch", "Aspen", "Maple",
"Rowan", "Alder", "Elm", "Oak", "Pine",
"Spruce", "Juniper", "Laurel", "Ivy", "Fern",
"Moss", "Lichen", "Thistle", "Clover", "Briar",
"Thorn", "Reed", "Sage", "Basil", "Lotus",
"Orchid", "Dahlia", "Iris", "Lily", "Violet",
"Jasmine", "Heather", "Daisy", "Poppy", "Aster",
"Azalea", "Magnolia", "Wisteria", "Camellia", "Marigold",
"Hawk", "Falcon", "Eagle", "Osprey", "Heron",
"Crane", "Wren", "Finch", "Lark", "Robin",
"Sparrow", "Raven", "Crow", "Jay", "Dove",
"Swift", "Owl", "Kestrel", "Harrier", "Condor",
"Wolf", "Fox", "Bear", "Lynx", "Puma",
"Jaguar", "Panther", "Stag", "Elk", "Moose",
"Bison", "Badger", "Otter", "Mink", "Ermine",
"Viper", "Cobra", "Adder", "Gecko", "Newt",
"Coral", "Pearl", "Amber", "Flint", "Slate",
"Granite", "Basalt", "Quartz", "Mica", "Clay",
"Dune", "Mesa", "Butte", "Gorge", "Canyon",
"Ridge", "Summit", "Crest", "Peak", "Cliff",
"Bluff", "Dale", "Glen", "Vale", "Hollow",
"Meadow", "Prairie", "Tundra", "Taiga", "Savanna",
"Grove", "Copse", "Thicket", "Canopy", "Glade",
"Brook", "Creek", "Rivulet", "Spring", "Rapids",
"Cascade", "Torrent", "Eddy", "Delta", "Marsh",
"Bog", "Fen", "Mire", "Lagoon", "Oasis",
"Frost", "Dew", "Mist", "Haze", "Fog",
"Breeze", "Gust", "Gale", "Squall", "Tempest",
"Aurora", "Solstice", "Equinox", "Eclipse", "Dawn",
"Dusk", "Twilight", "Ember", "Spark", "Flame",
"Petal", "Bloom", "Blossom", "Sprout", "Seedling",
"Sapling", "Root", "Bark", "Sap", "Nectar",
"Pollen", "Spore", "Frond", "Tendril", "Vine",
"Bramble", "Nettle", "Sorrel", "Yarrow", "Tansy",
"Mushroom", "Fungi", "Truffle", "Morel", "Chanterelle"
],
"warriors": [
"Spartan", "Samurai", "Viking", "Shogun", "Ronin",
"Gladiator", "Centurion", "Legionary", "Tribune", "Praetor",
"Knight", "Paladin", "Templar", "Crusader", "Sentinel",
"Warden", "Marshal", "Captain", "General", "Admiral",
"Vanguard", "Raider", "Berserker", "Valkyrie", "Einherjar",
"Ninja", "Shinobi", "Kenshi", "Bushido", "Daimyo",
"Mongol", "Khan", "Sultan", "Shah", "Pharaoh",
"Legatus", "Consul", "Imperator", "Caesar", "Augustus",
"Hussar", "Cossack", "Janissary", "Mamluk", "Saracen",
"Apache", "Comanche", "Mohawk", "Zulu", "Aztec",
"Mayan", "Incan", "Sparrow", "Hawk", "Falcon",
"Talon", "Fang", "Claw", "Tusk", "Horn",
"Blade", "Saber", "Rapier", "Cutlass", "Katana",
"Claymore", "Scimitar", "Halberd", "Glaive", "Pike",
"Lance", "Javelin", "Spear", "Trident", "Mace",
"Flail", "Hammer", "Maul", "Axe", "Tomahawk",
"Dagger", "Stiletto", "Dirk", "Kunai", "Shuriken",
"Bow", "Longbow", "Crossbow", "Bolt", "Arrow",
"Quiver", "Shield", "Buckler", "Aegis", "Bulwark",
"Rampart", "Bastion", "Citadel", "Fortress", "Garrison",
"Helm", "Visor", "Gauntlet", "Bracer", "Greave",
"Cuirass", "Brigand", "Chainmail", "Plate", "Scale",
"Banner", "Standard", "Crest", "Sigil", "Herald",
"Siege", "Assault", "Charge", "Rally", "Flanker",
"Scout", "Ranger", "Sniper", "Archer", "Lancer",
"Dragoon", "Cavalry", "Infantry", "Legion", "Phalanx",
"Cohort", "Regiment", "Battalion", "Brigade", "Platoon",
"Striker", "Brawler", "Duelist", "Champion", "Conqueror",
"Warlord", "Overlord", "Chieftain", "Thane", "Jarl",
"Reaver", "Marauder", "Pillager", "Ravager", "Slayer",
"Titan", "Colossus", "Juggernaut", "Goliath", "Ajax",
"Hector", "Achilles", "Leonidas", "Hannibal", "Attila",
"Genghis", "Alexander", "Boudicca", "Shaka", "Saladin",
"Agincourt", "Thermopylae", "Troy", "Carthage", "Masada",
"Valor", "Honor", "Glory", "Fury", "Wrath"
],
"music": [
"Tempo", "Rhythm", "Melody", "Harmony", "Chord",
"Riff", "Solo", "Verse", "Chorus", "Bridge",
"Cadence", "Crescendo", "Forte", "Piano", "Allegro",
"Adagio", "Vivace", "Presto", "Staccato", "Legato",
"Vibrato", "Tremolo", "Glissando", "Arpeggio", "Pizzicato",
"Fermata", "Sforzando", "Rubato", "Ostinato", "Coda",
"Treble", "Bass", "Alto", "Tenor", "Soprano",
"Baritone", "Falsetto", "Contralto", "Aria", "Duet",
"Sonata", "Fugue", "Prelude", "Nocturne", "Etude",
"Opus", "Requiem", "Ballad", "Anthem", "Hymn",
"Lyric", "Serenade", "Caprice", "Fantasia", "Overture",
"Minuet", "Waltz", "Bolero", "Tango", "Mambo",
"Samba", "Rumba", "Salsa", "Swing", "Bebop",
"Funk", "Soul", "Blues", "Jazz", "Gospel",
"Grunge", "Punk", "Metal", "Rock", "Indie",
"Techno", "Trance", "House", "Dubstep", "Ambient",
"Synth", "Drone", "Fuzz", "Wah", "Reverb",
"Echo", "Loop", "Beat", "Drop", "Groove",
"Fiddle", "Banjo", "Lute", "Harp", "Lyre",
"Viola", "Cello", "Oboe", "Flute", "Fife",
"Bugle", "Cornet", "Trumpet", "Trombone", "Tuba",
"Sax", "Clarinet", "Bassoon", "Piccolo", "Organ",
"Cymbal", "Gong", "Chime", "Bell", "Snare",
"Tambourine", "Bongo", "Conga", "Djembe", "Tabla",
"Sitar", "Shamisen", "Koto", "Guzheng", "Erhu",
"Raga", "Mantra", "Chant", "Dirge", "Elegy",
"Refrain", "Motif", "Theme", "Encore", "Finale",
"Pitch", "Tone", "Note", "Scale", "Octave",
"Sharp", "Flat", "Major", "Minor", "Modal",
"Acoustic", "Electric", "Muted", "Resonant", "Harmonic",
"Sonic", "Stereo", "Mono", "Vinyl", "Track",
"Album", "Mixtape", "Jam", "Gig", "Venue",
"Stage", "Amp", "Speaker", "Mic", "Reed",
"Bow", "Pick", "Slide", "Hammer", "Mallet",
"Timpani", "Marimba", "Celesta", "Dulcimer", "Zither"
],
"literature": [
"Prose", "Verse", "Stanza", "Sonnet", "Haiku",
"Limerick", "Ballad", "Ode", "Epic", "Saga",
"Fable", "Myth", "Legend", "Tale", "Lore",
"Tome", "Codex", "Scroll", "Grimoire", "Almanac",
"Quill", "Ink", "Vellum", "Parchment", "Folio",
"Preface", "Prologue", "Epilogue", "Chapter", "Canto",
"Epitaph", "Elegy", "Dirge", "Psalm", "Hymn",
"Allegory", "Parable", "Satire", "Parody", "Irony",
"Motif", "Theme", "Trope", "Genre", "Canon",
"Muse", "Oracle", "Bard", "Scribe", "Sage",
"Hamlet", "Othello", "Prospero", "Oberon", "Titania",
"Puck", "Ariel", "Caliban", "Portia", "Cordelia",
"Macbeth", "Banquo", "Lear", "Juliet", "Romeo",
"Brutus", "Cassius", "Shylock", "Mercutio", "Falstaff",
"Gatsby", "Atticus", "Scout", "Holden", "Pip",
"Huck", "Ishmael", "Ahab", "Queequeg", "Dorian",
"Darcy", "Heathcliff", "Rochester", "Eyre", "Estella",
"Cosette", "Valjean", "Quixote", "Sancho", "Dulcinea",
"Odysseus", "Aeneas", "Beowulf", "Gilgamesh", "Roland",
"Percival", "Galahad", "Lancelot", "Gawain", "Merlin",
"Scheherazade", "Sinbad", "Aladdin", "Mowgli", "Baloo",
"Nemo", "Moreau", "Jekyll", "Hyde", "Dracula",
"Renfield", "Harker", "Shelley", "Bronte", "Austen",
"Poe", "Frost", "Wilde", "Twain", "Dickens",
"Tolstoy", "Kafka", "Orwell", "Hemingway", "Faulkner",
"Woolf", "Plath", "Dumas", "Hugo", "Verne",
"Cipher", "Riddle", "Enigma", "Rune", "Glyph",
"Sigil", "Symbol", "Token", "Emblem", "Crest",
"Aria", "Lyric", "Rhyme", "Meter", "Cadence",
"Pathos", "Ethos", "Logos", "Hubris", "Nemesis",
"Catharsis", "Climax", "Zenith", "Nadir", "Pivot",
"Twist", "Reverie", "Whimsy", "Satyr", "Nymph",
"Sprite", "Wraith", "Phantom", "Specter", "Shade",
"Vestige", "Relic", "Echo", "Mirage", "Revenant",
"Requiem", "Opus", "Magnum", "Novella", "Lexicon"
],
"gems": [
"Ruby", "Sapphire", "Emerald", "Diamond", "Topaz",
"Amethyst", "Opal", "Pearl", "Garnet", "Peridot",
"Onyx", "Jade", "Agate", "Jasper", "Quartz",
"Citrine", "Zircon", "Beryl", "Spinel", "Tanzanite",
"Tourmaline", "Morganite", "Kunzite", "Iolite", "Larimar",
"Sunstone", "Moonstone", "Bloodstone", "Lodestone", "Heliodor",
"Chrysolite", "Alexandrite", "Andalusite", "Labradorite", "Amazonite",
"Rhodonite", "Sodalite", "Fluorite", "Calcite", "Pyrite",
"Galena", "Bauxite", "Magnetite", "Hematite", "Malachite",
"Azurite", "Turquoise", "Lapis", "Lazuli", "Carnelian",
"Sardonyx", "Chalcedony", "Aventurine", "Obsidian", "Granite",
"Marble", "Basalt", "Pumice", "Slate", "Flint",
"Amber", "Coral", "Ivory", "Jet", "Nacre",
"Gold", "Silver", "Platinum", "Copper", "Bronze",
"Iron", "Steel", "Titanium", "Cobalt", "Nickel",
"Chrome", "Zinc", "Tin", "Lead", "Brass",
"Pewter", "Electrum", "Palladium", "Rhodium", "Iridium",
"Osmium", "Ruthenium", "Tungsten", "Bismuth", "Lithium",
"Cesium", "Radium", "Gallium", "Indium", "Thallium",
"Strontium", "Barium", "Cadmium", "Antimony", "Tellurium",
"Selenite", "Celestite", "Sphalerite", "Kyanite", "Prehnite",
"Apatite", "Danburite", "Howlite", "Lepidolite", "Sugilite",
"Charoite", "Seraphinite", "Moldavite", "Tektite", "Shungite",
"Crystal", "Prism", "Facet", "Carat", "Luster",
"Brilliance", "Clarity", "Sparkle", "Shimmer", "Glimmer",
"Gleam", "Sheen", "Polish", "Ore", "Vein",
"Nugget", "Ingot", "Alloy", "Forge", "Smelt",
"Temper", "Anneal", "Refine", "Crucible", "Slag",
"Geode", "Druzy", "Cabochon", "Cameo", "Inlay",
"Filigree", "Gilt", "Patina", "Verdigris", "Tarnish",
"Shard", "Sliver", "Splinter", "Fragment", "Chip",
"Matrix", "Stratum", "Seam", "Deposit", "Quarry",
"Cavern", "Grotto", "Trove", "Cache", "Vault",
"Crown", "Diadem", "Tiara", "Circlet", "Coronet",
"Amulet", "Talisman", "Pendant", "Brooch", "Signet"
],
"ocean": [
"Tide", "Wave", "Surf", "Swell", "Crest",
"Trough", "Breaker", "Ripple", "Current", "Drift",
"Riptide", "Undertow", "Maelstrom", "Whirlpool", "Eddy",
"Tsunami", "Surge", "Deluge", "Torrent", "Cascade",
"Coral", "Reef", "Atoll", "Lagoon", "Shoal",
"Sandbar", "Islet", "Archipelago", "Fjord", "Inlet",
"Cove", "Bay", "Gulf", "Sound", "Strait",
"Channel", "Harbor", "Marina", "Pier", "Wharf",
"Dock", "Jetty", "Quay", "Beacon", "Lighthouse",
"Anchor", "Helm", "Rudder", "Keel", "Hull",
"Bow", "Stern", "Port", "Mast", "Sail",
"Rigging", "Galley", "Frigate", "Sloop", "Schooner",
"Clipper", "Brigantine", "Corsair", "Buccaneer", "Mariner",
"Skipper", "Bosun", "Navigator", "Helmsman", "Admiral",
"Commodore", "Captain", "Privateer", "Seafarer", "Voyager",
"Kraken", "Leviathan", "Siren", "Selkie", "Nereid",
"Triton", "Poseidon", "Neptune", "Calypso", "Charybdis",
"Scylla", "Davy", "Locker", "Nautilus", "Nemo",
"Whale", "Orca", "Dolphin", "Narwhal", "Porpoise",
"Shark", "Barracuda", "Marlin", "Sailfish", "Swordfish",
"Manta", "Stingray", "Moray", "Grouper", "Snapper",
"Tuna", "Mackerel", "Herring", "Sardine", "Anchovy",
"Squid", "Octopus", "Ammonite", "Cuttlefish", "Jellyfish",
"Anemone", "Urchin", "Starfish", "Seahorse", "Conch",
"Abalone", "Clam", "Oyster", "Mussel", "Scallop",
"Lobster", "Crab", "Shrimp", "Barnacle", "Kelp",
"Seaweed", "Plankton", "Algae", "Brine", "Foam",
"Spray", "Spume", "Mist", "Salt", "Pearl",
"Trench", "Abyss", "Fathom", "Depth", "Benthic",
"Pelagic", "Abyssal", "Hadal", "Tidal", "Littoral",
"Estuary", "Delta", "Mangrove", "Marsh", "Wetland",
"Driftwood", "Flotsam", "Jetsam", "Wreck", "Salvage",
"Bounty", "Plunder", "Treasure", "Doubloon", "Cutlass",
"Cannon", "Broadside", "Mainsail", "Spinnaker", "Jib",
"Bowsprit", "Gangway", "Porthole", "Bulkhead", "Bilge"
],
"weather": [
"Storm", "Tempest", "Squall", "Gale", "Gust",
"Breeze", "Zephyr", "Draft", "Whirlwind", "Tornado",
"Cyclone", "Typhoon", "Hurricane", "Monsoon", "Sirocco",
"Mistral", "Chinook", "Foehn", "Harmattan", "Tramontane",
"Thunder", "Lightning", "Bolt", "Flash", "Strike",
"Rumble", "Crack", "Boom", "Clap", "Roar",
"Rain", "Drizzle", "Shower", "Downpour", "Deluge",
"Torrent", "Cloudburst", "Sprinkle", "Mizzle", "Sleet",
"Snow", "Blizzard", "Flurry", "Frost", "Ice",
"Hail", "Rime", "Hoarfrost", "Glaze", "Verglas",
"Fog", "Mist", "Haze", "Smog", "Murk",
"Overcast", "Gloom", "Shadow", "Shade", "Dusk",
"Cloud", "Cumulus", "Stratus", "Cirrus", "Nimbus",
"Cumulonimbus", "Alto", "Wisp", "Veil", "Canopy",
"Anvil", "Billow", "Plume", "Column", "Front",
"Ridge", "Trough", "Vortex", "Eye", "Funnel",
"Twister", "Spout", "Surge", "Flood", "Flicker",
"Cascade", "Gush", "Swell", "Crest", "Tide",
"Drought", "Arid", "Parch", "Scorch", "Blaze",
"Ember", "Kindle", "Sear", "Wither", "Dustbowl",
"Heat", "Warmth", "Balmy", "Swelter", "Muggy",
"Humid", "Sultry", "Tropic", "Equator", "Solstice",
"Equinox", "Aurora", "Borealis", "Mirage", "Shimmer",
"Glint", "Gleam", "Radiance", "Glow", "Halo",
"Rainbow", "Prism", "Spectrum", "Arc", "Bow",
"Dew", "Droplet", "Puddle", "Rivulet", "Stream",
"Thaw", "Melt", "Freeze", "Chill", "Nip",
"Bite", "Brisk", "Crisp", "Cool", "Cold",
"Frigid", "Glacial", "Polar", "Arctic", "Tundra",
"Permafrost", "Icecap", "Avalanche", "Snowdrift", "Whiteout",
"Barometer", "Pressure", "Isobar", "Celsius", "Kelvin",
"Climate", "Season", "Forecast", "Outlook", "Pattern",
"Windchill", "Dewpoint", "Updraft", "Downdraft", "Jetstream",
"Tradewind", "Doldrums", "Lull", "Calm", "Serene",
"Clear", "Sunny", "Bright", "Radiant", "Blazing"
]
}

View File

@@ -0,0 +1,45 @@
import json
import random
from functools import lru_cache
from pathlib import Path
DICTIONARY_PATH = Path(__file__).resolve().parents[1] / "seeds" / "data" / "name_dictionary.json"
@lru_cache(maxsize=1)
def _load_dictionary() -> dict[str, list[str]]:
if not DICTIONARY_PATH.exists():
return {}
with open(DICTIONARY_PATH) as f:
return json.load(f)
def get_naming_categories() -> list[str]:
"""Return sorted list of available naming category names."""
return sorted(_load_dictionary().keys())
def get_words_for_category(category: str) -> list[str]:
"""Return the word list for a category, or empty list if not found."""
return _load_dictionary().get(category, [])
def suggest_names(
category: str,
used_names: set[str],
count: int = 10,
) -> list[str]:
"""Pick random name suggestions from a category, excluding used names.
Returns up to `count` names. If the category is nearly exhausted,
returns fewer.
"""
words = get_words_for_category(category)
if not words:
return []
available = [w for w in words if w not in used_names]
if not available:
return []
return random.sample(available, min(count, len(available)))

View File

@@ -0,0 +1,84 @@
from unittest.mock import patch
import pytest
from app.services.naming import (
get_naming_categories,
get_words_for_category,
suggest_names,
)
MOCK_DICTIONARY = {
"mythology": ["Apollo", "Athena", "Loki", "Thor", "Zeus"],
"food": ["Basil", "Sage", "Pepper", "Saffron", "Mango"],
"space": ["Apollo", "Nova", "Nebula", "Comet", "Vega"],
}
@pytest.fixture(autouse=True)
def _mock_dictionary():
with patch("app.services.naming._load_dictionary", return_value=MOCK_DICTIONARY):
yield
class TestGetNamingCategories:
def test_returns_sorted_categories(self):
result = get_naming_categories()
assert result == ["food", "mythology", "space"]
def test_returns_empty_for_empty_dictionary(self):
with patch("app.services.naming._load_dictionary", return_value={}):
assert get_naming_categories() == []
class TestGetWordsForCategory:
def test_returns_words_for_valid_category(self):
result = get_words_for_category("mythology")
assert result == ["Apollo", "Athena", "Loki", "Thor", "Zeus"]
def test_returns_empty_for_unknown_category(self):
assert get_words_for_category("nonexistent") == []
class TestSuggestNames:
def test_returns_requested_count(self):
result = suggest_names("mythology", set(), count=3)
assert len(result) == 3
assert all(name in MOCK_DICTIONARY["mythology"] for name in result)
def test_excludes_used_names(self):
used = {"Apollo", "Athena", "Loki"}
result = suggest_names("mythology", used, count=10)
assert set(result) <= {"Thor", "Zeus"}
assert not set(result) & used
def test_returns_fewer_when_category_nearly_exhausted(self):
used = {"Apollo", "Athena", "Loki", "Thor"}
result = suggest_names("mythology", used, count=10)
assert result == ["Zeus"]
def test_returns_empty_when_category_fully_exhausted(self):
used = {"Apollo", "Athena", "Loki", "Thor", "Zeus"}
result = suggest_names("mythology", used, count=10)
assert result == []
def test_returns_empty_for_unknown_category(self):
result = suggest_names("nonexistent", set(), count=10)
assert result == []
def test_no_duplicates_in_suggestions(self):
result = suggest_names("mythology", set(), count=5)
assert len(result) == len(set(result))
def test_default_count_is_ten(self):
# food has 5 words, so we should get all 5
result = suggest_names("food", set())
assert len(result) == 5
def test_cross_category_names_handled_independently(self):
# "Apollo" used in mythology shouldn't affect space
used = {"Apollo"}
mythology_result = suggest_names("mythology", used, count=10)
space_result = suggest_names("space", used, count=10)
assert "Apollo" not in mythology_result
assert "Apollo" not in space_result