diff --git a/Readme.md b/Readme.md index bd533a4..72bf3c9 100644 --- a/Readme.md +++ b/Readme.md @@ -8,8 +8,14 @@ scripts/ ├── dice_roller/ # Скрипты для бросков кубиков ├── faction_turns/ # Система ходов фракций (WWN) +│ ├── propose_faction_turn.py # Генерация доступных действий +│ ├── resolve_faction_turn.py # Применение ходов фракций +│ ├── faction_utils.py # Вспомогательные функции фракций +│ ├── wwn_mechanics.py # Механики системы +│ └── wwn_assets_data.py # Данные активов ├── generate_encounter/ # Генераторы столкновений и энкаунтеров ├── generate_npc/ # Генерация NPC (био, классы, характеристики) +│ └── regions.py # Региональные данные и symspellpy (населенные пункты) ├── generate_trap/ # Генерация ловушек ├── generate_treasure/ # Генерация лута и сокровищ ├── time/ # Управление временем кампании diff --git a/TASKS.md b/TASKS.md index 164f103..051206e 100644 --- a/TASKS.md +++ b/TASKS.md @@ -1,36 +1,20 @@ -# План работ по рефакторингу API и расширению тестов +# Задачи по рефакторингу локаций и тулов фракций WWN -## 1. Рефакторинг vault_utils.py -- [x] Заменить единый `vault_query` с хардкодом строк на экспортируемые прямые функции. -- [x] Обновить XML-doc (docstrings) для всех методов. -- [x] Добавить универсальную функцию `get_recent_files` (на базе JsonLogic). +- [x] **Этап 1: Обновление regions.py** + - [x] Вынести часть населённых пунктов из REGIONAL_OVERWRITES в SETTLEMENTS_OVERWRITES (`D:/Work/Software_Development/Projects/ttrpg-obsidian-vault/scripts/generate_npc/regions.py`) + - [x] Настроить динамическое вливание SETTLEMENTS_OVERWRITES в `REGIONAL_OVERWRITES` (добавление в `aliases`) + - [x] Сформировать словари `WORLD_SETTLEMENTS` и функцию коррекции локаций через `symspellpy` + - [x] Полностью перенести все населенные пункты из `REGIONAL_OVERWRITES` в `SETTLEMENTS_OVERWRITES` без исключений. В `aliases` оставить только синонимы регионов и подрегионы. -## 2. Рефакторинг faction_utils.py -- [x] Перевести вызовы с `vault_query(...)` на прямые вызовы API. -- [x] Переписать `get_all_factions` и `get_recent_events` как обёртки вокруг `get_recent_files`. +- [x] **Этап 2: Обновление faction_turns (propose_faction_turn & resolve_faction_turn)** + - [x] Обновить `propose_faction_turn.py` (`world` + `location` вместо `location_wikilink` + коррекция через `symspellpy`) + - [x] Обновить `resolve_faction_turn.py` (`world` + `location` вместо `location_wikilink` + коррекция через `symspellpy`) + - [x] Проверить и синхронизировать `faction_utils.py` (существующая логика совместима) -## 3. Рефакторинг time_utils.py -- [x] Обновить импорты из `vault_utils`. -- [x] Заменить `vault_query` на прямые методы (`get_file`, `write_file`). +- [x] **Этап 3: Обновление README.md** + - [x] Обновить структуру и добавить блок для ИИ-агентов (уже присутствует) -## 4. Исследование Obsidian Local REST API -- [x] Добавить примеры JSONLogic-запросов и PowerShell-скрипт с сортировкой. -- [x] Очистить документацию от сбоев форматирования Markdown ссылок. - -## 5. Расширение тестов фракций -- [x] Дописать интеграционные тесты для `get_recent_events`, `get_location` и работы с активами. -- [x] Расширить тесты логики ходов фракций в `test_wwn_mechanics.py`. -- [x] Добавлены setup-тесты для перезаписи тестовых файлов фракций (frontmatter и контент) для детерминированности среды. -- [x] Убедиться, что текущий набор pytest проходит без ошибок (`pytest -v`). -# План работ и задачи - -## Исправление ошибок импорта фракций -- [x] Удалить импорт `vault_query` в `D:/Work/Software_Development/Projects/ttrpg-obsidian-vault/scripts/faction_turns/resolve_faction_turn.py` -- [x] Удалить импорт `vault_query` в `D:/Work/Software_Development/Projects/ttrpg-obsidian-vault/scripts/faction_turns/propose_faction_turn.py` -- [x] Заменить вызов `vault_query` на прямые вызовы функций в `D:/Work/Software_Development/Projects/ttrpg-obsidian-vault/scripts/faction_turns/tests/verify_factions.py` - -## Исправления логики фракций -- [x] Починить логику `Repair Asset` для ремонта `Base of Influence` (цель может не иметь точного совпадения имени) в файле `resolve_faction_turn.py` -- [x] Сделать генерацию названий файлов событий (Event files) более читаемой, убрать длинные подчеркивания и дефисы. -- [✓] Добавить тесты исцеления Base of Influence для обычной и мощной фракций (D:/Work/Software_Development/Projects/ttrpg-obsidian-vault/scripts/faction_turns/tests/test_repair_boi.py) -- [✓] Запустить и проверить прохождение тестов `pytest` +- [x] **Этап 4: Тестирование** + - [x] Добавить тесты для `generate_npc` (написаны). + - [x] Добавить тесты для `generate_encounter` (написаны). + - [x] Добавить тесты для функции нечеткого сопоставления локаций (`match_location`). \ No newline at end of file diff --git a/faction_turns/propose_faction_turn.py b/faction_turns/propose_faction_turn.py index 394d9c6..9f50c9a 100644 --- a/faction_turns/propose_faction_turn.py +++ b/faction_turns/propose_faction_turn.py @@ -68,17 +68,22 @@ from wwn_mechanics import ( # TypedDict # --------------------------------------------------------------------------- +from generate_npc.regions import match_location + class FactionTurnProposalRequest(TypedDict, total=False): """ Параметры одного запроса на генерацию хода фракций. Поля: - location_wikilink: Имя локации на русском языке в формате Obsidian wikilink. - Обязательное поле. - КРИТИЧЕСКИ ВАЖНО: Копируй название буква в букву из запроса, - СТРОГО в именительном падеже. ЗАПРЕЩЕНО склонять, менять - окончания, переводить или придумывать буквы. - Пример: "[[Даггерфорд]]", "[[Калимшан]]", название локации на русском. + world: Космологический уровень сцены (англ.). Прайм-мир + ("Toril", "Krynn", "Oerth") ЛИБО план существования. + По умолчанию: "Toril". + + location: Имя локации на русском/английском языке. + Обязательное поле. Автоматически корректируется symspellpy. + Пример: "Даггерфорд", "Калимшан", "Waterdeep". + + location_wikilink: Оставлено для обратной совместимости. factions_folder: Путь к папке с файлами фракций внутри vault. По умолчанию: "Фракции". @@ -94,7 +99,9 @@ class FactionTurnProposalRequest(TypedDict, total=False): Допустимы: 1–20. По умолчанию: 5. """ - location_wikilink: str # wikilink локации, обязательно: "[[Уотердип]]" + world: str # "Toril", "Krynn", "Oerth", ... + location: str # "Waterdeep" + location_wikilink: str # fallback factions_folder: str # default "Фракции" events_folder: str # default "События" game_date: str # default: today @@ -108,7 +115,8 @@ class FactionTurnProposalRequest(TypedDict, total=False): def _get_default_request() -> FactionTurnProposalRequest: """Возвращает запрос с дефолтными значениями.""" return { - "location_wikilink": "[[Unknown Location]]", + "world": "Toril", + "location": "Unknown Location", "factions_folder": "Фракции", "events_folder": "События", "game_date": date.today().strftime("%d %B %Y"), @@ -770,7 +778,20 @@ def propose_faction_turn( for i, raw_req in enumerate(requests, start=1): req = _merge_with_defaults(raw_req) - location_wikilink = req["location_wikilink"] + world = req.get("world", "Toril") + + # Поддержка обратной совместимости с location_wikilink + raw_location = req.get("location") + if raw_location == "Unknown Location" and req.get("location_wikilink") != "[[Unknown Location]]": + raw_location = req.get("location_wikilink", "Unknown Location") + + # Очистка от скобок wikilink, если они есть + raw_location = raw_location.strip("[]") + + # Сопоставление локации через symspellpy + matched_location = match_location(world, raw_location) + location_wikilink = f"[[{matched_location}]]" + factions_folder = req["factions_folder"] events_folder = req["events_folder"] game_date = req["game_date"] diff --git a/faction_turns/resolve_faction_turn.py b/faction_turns/resolve_faction_turn.py index 758fd2e..a2443c5 100644 --- a/faction_turns/resolve_faction_turn.py +++ b/faction_turns/resolve_faction_turn.py @@ -78,6 +78,8 @@ from wwn_mechanics import ( # TypedDict # --------------------------------------------------------------------------- +from generate_npc.regions import match_location + class FactionAction(TypedDict, total=False): """ Параметры одного действия одной фракции за ход. @@ -92,9 +94,12 @@ class FactionAction(TypedDict, total=False): "Expand Influence", "Create Asset", "Hide Asset", "Sell Asset". Обязательное поле. - location: Локация на русском в формате Obsidian wikilink. - Обязательное поле. - Пример: "[[Уотердип]]", "[[Невервинтер]]", название локации на русском. + world: Космологический уровень сцены (англ.). + По умолчанию: "Toril". + + location: Имя локации на английском языке. + Обязательное поле. + Пример: "Waterdeep", "Neverwinter". attacking_asset: Имя атакующего актива (для Attack). Пример: "Organization Moles". @@ -128,7 +133,8 @@ class FactionAction(TypedDict, total=False): """ faction_file: str # имя файла без .md, обязательно action_type: str # "Attack"|"Move Asset"|"Repair Asset"|"Expand Influence"|"Create Asset"|"Hide Asset"|"Sell Asset" - location: str # "[[Уотердип]]" + world: str # "Toril", "Krynn"... + location: str # "Waterdeep" attacking_asset: str # для Attack target_faction: str # имя файла цели без .md, для Attack target_asset: str # имя целевого актива, для Attack @@ -1062,6 +1068,20 @@ def resolve_faction_turn( faction_files_needed = set() for action in actions: + # Обработка location через symspellpy + world = action.get("world", "Toril") + raw_loc = action.get("location", "") + if raw_loc: + clean_loc = raw_loc.strip("[]") + matched_loc = match_location(world, clean_loc) + action["location"] = f"[[{matched_loc}]]" + + raw_exp_loc = action.get("expand_location", "") + if raw_exp_loc: + clean_exp_loc = raw_exp_loc.strip("[]") + matched_exp_loc = match_location(world, clean_exp_loc) + action["expand_location"] = f"[[{matched_exp_loc}]]" + faction_files_needed.add(action.get("faction_file", "")) if action.get("action_type") == "Attack": faction_files_needed.add(action.get("target_faction", "")) diff --git a/generate_encounter/generate_encounter.py b/generate_encounter/gen_encounter.py similarity index 99% rename from generate_encounter/generate_encounter.py rename to generate_encounter/gen_encounter.py index 011a280..b8b4126 100644 --- a/generate_encounter/generate_encounter.py +++ b/generate_encounter/gen_encounter.py @@ -56,7 +56,7 @@ from get_npc_races import get_npc_races # Импортируем функцию из соседней папки. # Поскольку в sys.path есть npc_folder, можно импортировать напрямую из файла -from generate_npc import generate_npc as get_npc +from gen_npc import generate_npc as get_npc # Множество планов существования (не прайм-миры). diff --git a/generate_encounter/tests/test_generate_encounter.py b/generate_encounter/tests/test_generate_encounter.py new file mode 100644 index 0000000..5e130ef --- /dev/null +++ b/generate_encounter/tests/test_generate_encounter.py @@ -0,0 +1,60 @@ +import sys +from pathlib import Path + +scripts_path = Path(__file__).resolve().parent.parent.parent +if str(scripts_path) not in sys.path: + sys.path.insert(0, str(scripts_path)) + +enc_path = Path(__file__).resolve().parent.parent +if str(enc_path) not in sys.path: + sys.path.insert(0, str(enc_path)) + +from gen_encounter import generate_encounter + +# ----------------------------------------------- Тесты Генерации Энкаунтеров ------------------------------------------------------------ + +def test_encounter_default(): + """ + Проверяет базовую генерацию случайной встречи с настройками по умолчанию. + Возвращает непустую строку-отчет. + """ + result = generate_encounter("тест", [{}]) + assert isinstance(result, str) + assert "Встреча №1" in result + assert "**Уровень группы:** 1" in result + +def test_encounter_specific_biome(): + """ + Проверяет генерацию встречи для конкретного биома, уровня и типа PvP. + Гарантирует, что параметры запроса применяются корректно. + """ + req = { + "environment": "urban", + "level": 5, + "encounter_type": "PvP" + } + result = generate_encounter("тест", [req]) + assert isinstance(result, str) + assert "**Биом:** Urban" in result + assert "**Тип встречи:** PvP" in result + assert "**Уровень группы:** 5" in result + +def test_encounter_planar_world(): + """ + Проверяет планарные миры. Биом должен игнорироваться при выборе таблиц. + Вывод должен содержать информацию о мире/плане. + """ + req = { + "world": "feywild", + "level": 8 + } + result = generate_encounter("тест", [req]) + assert isinstance(result, str) + assert "**Мир/План:** feywild" in result + assert "**Уровень группы:** 8" in result + +if __name__ == "__main__": + test_encounter_default() + test_encounter_specific_biome() + test_encounter_planar_world() + print("Все тесты generate_encounter успешно пройдены!") diff --git a/generate_npc/generate_npc.py b/generate_npc/gen_npc.py similarity index 99% rename from generate_npc/generate_npc.py rename to generate_npc/gen_npc.py index 8959ffd..4239ea2 100644 --- a/generate_npc/generate_npc.py +++ b/generate_npc/gen_npc.py @@ -235,7 +235,7 @@ def _generate_single_npc( subrace_name = _weighted_pick(region_subs) elif "subraces" in race_data and race_data["subraces"]: sub_pool = _normalize_chances( - {s: d["chance"] for s, d in race_data["subraces"].items()} + {s: d.get("chance", 10.0) for s, d in race_data["subraces"].items()} ) subrace_name = _weighted_pick(sub_pool) race_obj = {"race": race_name, "subrace": subrace_name} diff --git a/generate_npc/regions.py b/generate_npc/regions.py index 301a5c9..c5f5da5 100644 --- a/generate_npc/regions.py +++ b/generate_npc/regions.py @@ -11,8 +11,7 @@ REGIONAL_OVERWRITES: dict = { "Toril": { "sword_coast_north": { - "aliases": ["Sword Coast North", "Savage Frontier", "Frozenfar", - "Luskan", "Neverwinter", "Port Llast", "Mirabar"], + "aliases": ["Sword Coast North", "Savage Frontier", "Frozenfar"], "chances": { "Human": {"chance": 58.0, "subraces": {"Illuskan": 70.0, "Damaran": 20.0, "Chondathan": 10.0}}, "Dwarf": {"chance": 14.0, "subraces": {"Shield/Mountain": 90.0, "Gold/Hill": 10.0}}, @@ -26,8 +25,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "silver_marches": { - "aliases": ["Silver Marches", "Luruar", "Silverymoon", "Sundabar", - "Mithral Hall", "Everlund", "Citadel Adbar"], + "aliases": ["Silver Marches", "Luruar"], "chances": { "Human": {"chance": 42.0, "subraces": {"Illuskan": 50.0, "Chondathan": 40.0, "Damaran": 10.0}}, "Dwarf": {"chance": 22.0, "subraces": {"Shield/Mountain": 95.0, "Gold/Hill": 5.0}}, @@ -40,8 +38,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "western_heartlands": { - "aliases": ["Western Heartlands", "Elturgard", "Elturel", "Beregost", - "Candlekeep", "Sunset Mountains", "Scornubel"], + "aliases": ["Western Heartlands", "Elturgard", "Sunset Mountains"], "chances": { "Human": {"chance": 72.0, "subraces": {"Chondathan": 55.0, "Tethyrian": 35.0, "Calishite": 7.0, "Illuskan": 3.0}}, "Halfling": {"chance": 9.0, "subraces": {"Lightfoot": 85.0, "Ghostwise": 10.0, "Stout": 5.0}}, @@ -53,7 +50,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "waterdeep": { - "aliases": ["Waterdeep", "City of Splendors", "Crown of North"], + "aliases": ["Crown of North"], "chances": { "Human": {"chance": 60.0, "subraces": {"Illuskan": 35.0, "Tethyrian": 30.0, "Chondathan": 25.0, "Calishite": 5.0, "Shou": 5.0}}, "Half-Elf": {"chance": 9.0, "subraces": {"Moon": 75.0, "Sun": 15.0, "Wood": 10.0}}, @@ -67,7 +64,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "baldurs_gate": { - "aliases": ["Baldur's Gate", "Gate", "Baldurs Gate"], + "aliases": [], "chances": { "Human": {"chance": 68.0, "subraces": {"Tethyrian": 45.0, "Chondathan": 30.0, "Calishite": 15.0, "Illuskan": 7.0, "Damaran": 3.0}}, "Half-Elf": {"chance": 7.0, "subraces": {"Moon": 70.0, "Wood": 30.0}}, @@ -81,8 +78,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "cormyr": { - "aliases": ["Cormyr", "Forest Kingdom", "Suzail", "Marsember", - "Arabel", "Wheloon", "Purple Dragons"], + "aliases": ["Cormyr", "Forest Kingdom", "Purple Dragons"], "chances": { "Human": {"chance": 78.0, "subraces": {"Chondathan": 80.0, "Tethyrian": 12.0, "Damaran": 5.0, "Illuskan": 3.0}}, "Half-Elf": {"chance": 6.0, "subraces": {"Moon": 80.0, "Sun": 20.0}}, @@ -94,8 +90,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "amn": { - "aliases": ["Amn", "Athkatla", "Crimmor", "Eshpurta", - "Purskul", "Trademeet", "Merchant's Domain"], + "aliases": ["Amn", "Merchant's Domain"], "chances": { "Human": {"chance": 78.0, "subraces": {"Tethyrian": 45.0, "Calishite": 38.0, "Chondathan": 12.0, "Illuskan": 5.0}}, "Halfling": {"chance": 8.0, "subraces": {"Lightfoot": 85.0, "Stout": 15.0}}, @@ -107,8 +102,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "tethyr": { - "aliases": ["Tethyr", "Zazesspur", "Darromar", "Myratma", - "Ithmong", "Saradush", "Riatavin", "Country of Purple Marches"], + "aliases": ["Tethyr", "Country of Purple Marches"], "chances": { "Human": {"chance": 74.0, "subraces": {"Tethyrian": 70.0, "Calishite": 18.0, "Chondathan": 8.0, "Illuskan": 4.0}}, "Halfling": {"chance": 9.0, "subraces": {"Lightfoot": 80.0, "Stout": 20.0}}, @@ -120,7 +114,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "calimshan": { - "aliases": ["Calimshan", "Calimport", "Memnon", "Almraiven", "Keltar"], + "aliases": ["Calimshan"], "chances": { "Human": {"chance": 82.0, "subraces": {"Calishite": 88.0, "Tethyrian": 8.0, "Chondathan": 4.0}}, "Genasi": {"chance": 8.0, "subraces": {"Fire": 35.0, "Air": 35.0, "Earth": 15.0, "Water": 15.0}}, @@ -131,7 +125,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "chult": { - "aliases": ["Chult", "Port Nyanzaru", "Mezro", "Chultan Peninsula", "Omu"], + "aliases": ["Chult", "Chultan Peninsula"], "chances": { "Human": {"chance": 68.0, "subraces": {"Chultan": 90.0, "Tethyrian": 6.0, "Calishite": 4.0}}, "Tabaxi": {"chance": 14.0}, @@ -144,8 +138,7 @@ REGIONAL_OVERWRITES: dict = { }, "unapproachable_east": { "aliases": ["Unapproachable East", "Aglarond border", - "Thesk", "Great Dale", "Bezantur", "Two Stars", - "Phsant", "Milvarune"], + "Thesk", "Great Dale"], "chances": { "Human": {"chance": 78.0, "subraces": {"Mulan": 50.0, "Rashemi": 25.0, "Damaran": 20.0, "Chondathan": 5.0}}, "Half-Orc": {"chance": 7.0}, @@ -157,8 +150,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "rashemen": { - "aliases": ["Rashemen", "Mulsantir", "Immilmar", "Urling", - "Citadel Rashemar", "Mulptan", "Land of Berserkers", + "aliases": ["Rashemen", "Land of Berserkers", "Ashenwood", "Witches of Rashemen"], "chances": { "Human": {"chance": 84.0, "subraces": {"Rashemi": 80.0, "Damaran": 12.0, "Mulan": 8.0}}, @@ -171,9 +163,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "thay": { - "aliases": ["Thay", "Red Wizards", "Eltabbar", "Tyraturos", - "Amruthar", "Pyarados", "Surthay", "Nethjet", - "Thaymount", "High Thay"], + "aliases": ["Thay", "Red Wizards", "Thaymount", "High Thay"], "chances": { "Human": {"chance": 82.0, "subraces": {"Mulan": 55.0, "Rashemi": 30.0, "Chondathan": 8.0, "Damaran": 7.0}}, "Half-Orc": {"chance": 5.0}, @@ -186,8 +176,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "underdark": { - "aliases": ["Underdark", "Menzoberranzan", "Blingdenstone", - "Gracklstugh", "Deep Realm", "Sshamath", "Ched Nasad"], + "aliases": ["Underdark", "Deep Realm"], "chances": { "Elf": {"chance": 46.0, "subraces": {"Drow": 100.0}}, "Dwarf": {"chance": 20.0, "subraces": {"Duergar": 100.0}}, @@ -201,8 +190,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "moonsea": { - "aliases": ["Moonsea", "Zhentil Keep", "Melvaunt", "Hillsfar", - "Mulmaster", "Phlan", "Thentia", "Sembia border"], + "aliases": ["Moonsea", "Sembia border"], "chances": { "Human": {"chance": 74.0, "subraces": {"Chondathan": 45.0, "Damaran": 35.0, "Illuskan": 12.0, "Mulan": 8.0}}, "Half-Orc": {"chance": 7.0}, @@ -214,8 +202,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "dalelands": { - "aliases": ["Dalelands", "Shadowdale", "Mistledale", "Deepingdale", - "Battledale", "Daggerdale", "Archendale"], + "aliases": ["Dalelands"], "chances": { "Human": {"chance": 70.0, "subraces": {"Chondathan": 70.0, "Damaran": 20.0, "Illuskan": 10.0}}, "Half-Elf": {"chance": 9.0, "subraces": {"Moon": 65.0, "Wood": 35.0}}, @@ -227,8 +214,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "sembia": { - "aliases": ["Sembia", "Selgaunt", "Ordulin", "Saerloon", - "Yhaunn", "Daerlun", "Urmlaspyr"], + "aliases": ["Sembia"], "chances": { "Human": {"chance": 80.0, "subraces": {"Chondathan": 65.0, "Damaran": 20.0, "Tethyrian": 8.0, "Shou": 7.0}}, "Half-Elf": {"chance": 6.0, "subraces": {"Moon": 75.0, "Wood": 25.0}}, @@ -240,8 +226,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "vast": { - "aliases": ["Vast", "Ravens Bluff", "Tantras", "Calaunt", - "Procampur", "Sarbreen"], + "aliases": ["Vast"], "chances": { "Human": {"chance": 72.0, "subraces": {"Chondathan": 50.0, "Damaran": 30.0, "Illuskan": 12.0, "Mulan": 8.0}}, "Dwarf": {"chance": 8.0, "subraces": {"Shield/Mountain": 75.0, "Gold/Hill": 25.0}}, @@ -253,8 +238,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "vilhon_reach": { - "aliases": ["Vilhon Reach", "Turmish", "Chondath", "Hlondeth", - "Alaghon", "Arrabar", "Sespech", "Nimpeth"], + "aliases": ["Vilhon Reach", "Turmish", "Chondath"], "chances": { "Human": {"chance": 74.0, "subraces": {"Chondathan": 60.0, "Turami": 25.0, "Tethyrian": 10.0, "Mulan": 5.0}}, "Half-Elf": {"chance": 6.0, "subraces": {"Wood": 60.0, "Moon": 40.0}}, @@ -280,8 +264,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "old_empires": { - "aliases": ["Old Empires", "Mulhorand", "Unther", "Chessenta", - "Skuld", "Messemprar", "Cimbar", "Soorenar"], + "aliases": ["Old Empires", "Mulhorand", "Unther", "Chessenta"], "chances": { "Human": {"chance": 80.0, "subraces": {"Mulan": 70.0, "Chondathan": 15.0, "Turami": 10.0, "Chessentan": 5.0}}, "Half-Orc": {"chance": 5.0}, @@ -317,8 +300,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "moonshae_isles": { - "aliases": ["Moonshae Isles", "Moonshaes", "Gwynneth", "Alaron", - "Snowdown", "Norland", "Callidyrr", "Corwell"], + "aliases": ["Moonshae Isles", "Moonshaes", "Gwynneth", "Alaron", "Snowdown", "Norland"], "chances": { "Human": {"chance": 72.0, "subraces": {"Ffolk": 60.0, "Northlander": 30.0, "Illuskan": 10.0}}, "Half-Elf": {"chance": 8.0, "subraces": {"Moon": 60.0, "Wood": 40.0}}, @@ -370,8 +352,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "impiltur_and_bloodstone": { - "aliases": ["Impiltur", "Damara", "Bloodstone Lands", "Narfell", - "Vaasa", "Lyrabar", "Heliogabalus"], + "aliases": ["Impiltur", "Damara", "Bloodstone Lands", "Narfell", "Vaasa"], "chances": { "Human": {"chance": 78.0, "subraces": {"Damaran": 75.0, "Chondathan": 15.0, "Rashemi": 10.0}}, "Dwarf": {"chance": 8.0, "subraces": {"Shield/Mountain": 85.0, "Gold/Hill": 15.0}}, @@ -384,8 +365,7 @@ REGIONAL_OVERWRITES: dict = { }, "anauroch": { - "aliases": ["Anauroch", "Great Sand Sea", "Bedine", "Sword of North", - "Plain of Standing Stones", "Hidden Lake", "Shade Enclave"], + "aliases": ["Anauroch", "Great Sand Sea", "Bedine", "Sword of North", "Plain of Standing Stones", "Hidden Lake"], "chances": { "Human": {"chance": 80.0, "subraces": {"Bedine": 55.0, "Netherese": 25.0, "Illuskan": 12.0, "Damaran": 8.0}}, "Genasi": {"chance": 6.0, "subraces": {"Earth": 40.0, "Air": 30.0, "Fire": 30.0}}, @@ -397,8 +377,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "aglarond": { - "aliases": ["Aglarond", "Altumbel", "Velprintalar", "Yeshelmaar", - "Umbergoth", "Emmech"], + "aliases": ["Aglarond", "Altumbel", "Umbergoth"], "chances": { "Half-Elf": {"chance": 40.0, "subraces": {"Wood": 70.0, "Moon": 30.0}}, "Human": {"chance": 38.0, "subraces": {"Aglarondan": 60.0, "Damaran": 25.0, "Chondathan": 15.0}}, @@ -410,7 +389,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "lantan": { - "aliases": ["Lantan", "Lantanna", "Supai", "Sambar", "Adaerglass"], + "aliases": ["Lantan", "Lantanna", "Supai"], "chances": { "Human": {"chance": 68.0, "subraces": {"Lantanese": 80.0, "Chondathan": 12.0, "Calishite": 8.0}}, "Gnome": {"chance": 20.0, "subraces": {"Rock": 85.0, "Forest": 15.0}}, @@ -421,9 +400,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "sea_of_fallen_stars": { - "aliases": ["Sea of Fallen Stars", "Inner Sea", "Pirate Isles", - "Dragonmere", "Sea of Swords bound", "Immurk's Hold", - "Whamite Isles"], + "aliases": ["Sea of Fallen Stars", "Inner Sea", "Pirate Isles", "Dragonmere", "Sea of Swords bound", "Whamite Isles"], "chances": { "Human": {"chance": 66.0, "subraces": {"Chondathan": 35.0, "Illuskan": 25.0, "Tethyrian": 20.0, "Turami": 12.0, "Mulan": 8.0}}, "Half-Orc": {"chance": 8.0}, @@ -436,9 +413,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "sword_coast_central": { - "aliases": ["Sword Coast Central", "Daggerford", "Trade Way", - "Delimbiyr Vale", "Secomber", "Boareskyr Bridge", - "Misty Forest bound"], + "aliases": ["Sword Coast Central", "Trade Way", "Delimbiyr Vale", "Misty Forest bound"], "chances": { "Human": {"chance": 70.0, "subraces": {"Tethyrian": 40.0, "Chondathan": 35.0, "Illuskan": 20.0, "Calishite": 5.0}}, "Halfling": {"chance": 8.0, "subraces": {"Lightfoot": 80.0, "Stout": 20.0}}, @@ -451,8 +426,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "maztica": { - "aliases": ["Maztica", "True World", "Nexal", "Payit", "Kultaka", - "Huacli", "Far Payit", "Helmsport", "Ulatos"], + "aliases": ["Maztica", "True World", "Nexal", "Payit", "Kultaka", "Huacli", "Far Payit"], "chances": { "Human": {"chance": 80.0, "subraces": {"Nexalan": 65.0, "Payit": 20.0, "Calishite": 10.0, "Tethyrian": 5.0}}, "Lizardfolk": {"chance": 6.0}, @@ -465,10 +439,7 @@ REGIONAL_OVERWRITES: dict = { }, "icewind_dale": { - "aliases": ["Icewind Dale", "Ten Towns", "Bryn Shander", "Targos", - "Lonelywood", "Easthaven", "Termalaine", "Caer-Dineval", - "Caer-Konig", "Dougan's Hole", "Good Mead", "Reghed Glacier", - "Kelvin's Cairn", "Reghedmen"], + "aliases": ["Icewind Dale", "Reghed Glacier", "Kelvin's Cairn", "Reghedmen"], "chances": { "Human": {"chance": 55.0, "subraces": {"Illuskan": 60.0, "Reghed": 30.0, "Damaran": 10.0}}, "Dwarf": {"chance": 16.0, "subraces": {"Shield/Mountain": 95.0, "Gold/Hill": 5.0}}, @@ -480,8 +451,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "evermeet": { - "aliases": ["Evermeet", "Island of Evermeet", "Green Isle", - "Isle of Evermeet", "Leuthilspar"], + "aliases": ["Evermeet", "Island of Evermeet", "Green Isle", "Isle of Evermeet"], "chances": { "Elf": {"chance": 88.0, "subraces": {"Sun": 50.0, "Moon": 42.0, "Avariel": 5.0, "Eladrin": 3.0}}, "Half-Elf": {"chance": 8.0, "subraces": {"Sun": 60.0, "Moon": 40.0}}, @@ -490,9 +460,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "myth_drannor": { - "aliases": ["Myth Drannor", "City of Song", "Cormanthyr", - "Cormanthor", "Semberholme", "Tangled Trees", - "Elven Court"], + "aliases": ["Myth Drannor", "City of Song", "Cormanthyr", "Cormanthor", "Elven Court"], "chances": { "Elf": {"chance": 50.0, "subraces": {"Moon": 45.0, "Sun": 30.0, "Wood": 20.0, "Eladrin": 5.0}}, "Human": {"chance": 22.0, "subraces": {"Chondathan": 70.0, "Damaran": 20.0, "Illuskan": 10.0}}, @@ -504,9 +472,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "tashalar_and_shaar": { - "aliases": ["Tashalar", "Tashluta", "Lapaliiya", "Thindol", - "Samarach", "Shaar", "Great Rift", "Channath Vale", - "Sheirtalar"], + "aliases": ["Tashalar", "Lapaliiya", "Thindol", "Samarach", "Shaar", "Great Rift", "Channath Vale"], "chances": { "Human": {"chance": 55.0, "subraces": {"Chultan": 45.0, "Shaaran": 25.0, "Tethyrian": 20.0, "Calishite": 10.0}}, "Yuan-ti Pureblood": {"chance": 16.0}, @@ -519,8 +485,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "nimbral": { - "aliases": ["Nimbral", "Nimbral Isles", "Immurk's Hold south", - "Land of Flying Hunt", "Cathtyr"], + "aliases": ["Nimbral", "Nimbral Isles", "Immurk's Hold south", "Land of Flying Hunt"], "chances": { "Human": {"chance": 80.0, "subraces": {"Halruaan": 45.0, "Tethyrian": 30.0, "Illuskan": 15.0, "Calishite": 10.0}}, "Half-Elf": {"chance": 7.0, "subraces": {"Moon": 100.0}}, @@ -535,8 +500,7 @@ REGIONAL_OVERWRITES: dict = { "Krynn": { "abanasinia": { - "aliases": ["Abanasinia", "Solace", "Haven", "Que-Shu", - "Crystalmir Lake", "Sancrist bound"], + "aliases": ["Abanasinia", "Crystalmir Lake", "Sancrist bound"], "chances": { "Human": {"chance": 72.0, "subraces": {"Plainsfolk": 45.0, "Abanasinian": 45.0, "Solamnic": 10.0}}, "Half-Elf": {"chance": 8.0, "subraces": {"Qualinesti": 100.0}}, @@ -548,8 +512,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "solamnia": { - "aliases": ["Solamnia", "Palanthas", "Vingaard", "Kalaman", - "Sancrist", "Knights of Solamnia", "Solamnic Plains"], + "aliases": ["Solamnia", "Sancrist", "Knights of Solamnia", "Solamnic Plains"], "chances": { "Human": {"chance": 82.0, "subraces": {"Solamnic": 80.0, "Abanasinian": 12.0, "Plainsfolk": 8.0}}, "Dwarf": {"chance": 5.0, "subraces": {"Hylar": 60.0, "Neidar": 40.0}}, @@ -561,7 +524,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "silvanesti": { - "aliases": ["Silvanesti", "Silvanost", "Silvamori", "Elven Home"], + "aliases": ["Silvanesti", "Elven Home"], "chances": { "Elf": {"chance": 90.0, "subraces": {"Silvanesti": 90.0, "Kagonesti": 10.0}}, "Half-Elf": {"chance": 6.0, "subraces": {"Silvanesti": 100.0}}, @@ -570,7 +533,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "qualinesti": { - "aliases": ["Qualinesti", "Qualinost", "Pax Tharkas bound"], + "aliases": ["Qualinesti", "Pax Tharkas bound"], "chances": { "Elf": {"chance": 85.0, "subraces": {"Qualinesti": 85.0, "Kagonesti": 15.0}}, "Half-Elf": {"chance": 8.0, "subraces": {"Qualinesti": 100.0}}, @@ -580,8 +543,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "thorbardin": { - "aliases": ["Thorbardin", "Mount Cloudseeker", "Hylar Halls", - "Kingdom of Dwarves", "Zhakar"], + "aliases": ["Thorbardin", "Mount Cloudseeker", "Hylar Halls", "Kingdom of Dwarves"], "chances": { "Dwarf": {"chance": 88.0, "subraces": {"Hylar": 40.0, "Daewar": 25.0, "Klar": 15.0, "Theiwar": 15.0, "Daergar": 5.0}}, "Gnome": {"chance": 6.0, "subraces": {"Tinker": 100.0}}, @@ -604,8 +566,7 @@ REGIONAL_OVERWRITES: dict = { }, "goodlund_and_balifor": { - "aliases": ["Goodlund", "Balifor", "Kendermore", "Port Balifor", - "Flotsam bound", "Kenderhome"], + "aliases": ["Goodlund", "Balifor", "Flotsam bound", "Kenderhome"], "chances": { "Kender": {"chance": 62.0}, "Human": {"chance": 22.0, "subraces": {"Plainsfolk": 60.0, "Abanasinian": 25.0, "Solamnic": 15.0}}, @@ -629,8 +590,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "neraka_and_taman_busuk": { - "aliases": ["Neraka", "Taman Busuk", "Sanction", "Dargaard", - "Estwilde", "Dargaard Keep bound"], + "aliases": ["Neraka", "Taman Busuk", "Dargaard", "Estwilde", "Dargaard Keep bound"], "chances": { "Human": {"chance": 60.0, "subraces": {"Solamnic": 40.0, "Plainsfolk": 35.0, "Abanasinian": 25.0}}, "Half-Ogre": {"chance": 14.0}, @@ -643,8 +603,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "nordmaar_and_khur": { - "aliases": ["Nordmaar", "Khur", "North Keep", "Willik", - "Wendle", "Kernaghan", "Khurish nomads"], + "aliases": ["Nordmaar", "Khur", "Khurish nomads"], "chances": { "Human": {"chance": 78.0, "subraces": {"Plainsfolk": 55.0, "Abanasinian": 25.0, "Solamnic": 20.0}}, "Half-Elf": {"chance": 6.0, "subraces": {"Kagonesti": 100.0}}, @@ -659,8 +618,7 @@ REGIONAL_OVERWRITES: dict = { "Oerth": { "greyhawk_city": { - "aliases": ["Greyhawk", "City of Greyhawk", "Free City of Greyhawk", - "Gem of Flanaess"], + "aliases": ["Free City of Greyhawk", "Gem of Flanaess"], "chances": { "Human": {"chance": 66.0, "subraces": {"Oeridian": 40.0, "Suel": 25.0, "Flan": 20.0, "Baklunish": 15.0}}, "Half-Elf": {"chance": 8.0, "subraces": {"Gray": 60.0, "High": 40.0}}, @@ -673,8 +631,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "keoland_and_sheldomar": { - "aliases": ["Keoland", "Sheldomar Valley", "Niole Dra", - "Gran March", "Bissel", "Ulek", "Yeomanry"], + "aliases": ["Keoland", "Sheldomar Valley", "Gran March", "Bissel", "Ulek", "Yeomanry"], "chances": { "Human": {"chance": 70.0, "subraces": {"Suel": 45.0, "Oeridian": 40.0, "Flan": 15.0}}, "Dwarf": {"chance": 9.0, "subraces": {"Hill": 55.0, "Mountain": 45.0}}, @@ -686,8 +643,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "furyondy_and_shield_lands": { - "aliases": ["Furyondy", "Chendl", "Shield Lands", "Veluna", - "Dyvers", "Willip"], + "aliases": ["Furyondy", "Shield Lands", "Veluna"], "chances": { "Human": {"chance": 74.0, "subraces": {"Oeridian": 55.0, "Suel": 25.0, "Flan": 20.0}}, "Dwarf": {"chance": 7.0, "subraces": {"Hill": 60.0, "Mountain": 40.0}}, @@ -699,8 +655,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "pomarj_and_wild_coast": { - "aliases": ["Pomarj", "Wild Coast", "Highport", "Suderham", - "Slave Lords bound"], + "aliases": ["Pomarj", "Wild Coast", "Slave Lords bound"], "chances": { "Human": {"chance": 40.0, "subraces": {"Oeridian": 40.0, "Suel": 35.0, "Flan": 25.0}}, "Orc": {"chance": 20.0}, @@ -727,8 +682,7 @@ REGIONAL_OVERWRITES: dict = { }, "great_kingdom_of_aerdy": { - "aliases": ["Great Kingdom", "Aerdy", "Rauxes", "North Province", - "South Province", "Ahlissa", "North Kingdom", "Rel Astra"], + "aliases": ["Great Kingdom", "Aerdy", "North Province", "South Province", "Ahlissa", "North Kingdom"], "chances": { "Human": {"chance": 74.0, "subraces": {"Oeridian": 65.0, "Suel": 20.0, "Flan": 15.0}}, "Dwarf": {"chance": 7.0, "subraces": {"Hill": 55.0, "Mountain": 45.0}}, @@ -741,8 +695,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "nyrond_and_urnsts": { - "aliases": ["Nyrond", "Rel Mord", "County of Urnst", "Duchy of Urnst", - "Womtham", "Almor", "Innspa bound"], + "aliases": ["Nyrond", "County of Urnst", "Duchy of Urnst", "Almor", "Innspa bound"], "chances": { "Human": {"chance": 72.0, "subraces": {"Oeridian": 50.0, "Suel": 30.0, "Flan": 20.0}}, "Dwarf": {"chance": 8.0, "subraces": {"Hill": 60.0, "Mountain": 40.0}}, @@ -754,8 +707,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "baklunish_west": { - "aliases": ["Baklunish West", "Ket", "Tusmit", "Ekbir", "Zeif", - "Ull", "Paynims", "Plains of Paynims", "Lopolla"], + "aliases": ["Baklunish West", "Ket", "Tusmit", "Ekbir", "Zeif", "Ull", "Paynims", "Plains of Paynims"], "chances": { "Human": {"chance": 82.0, "subraces": {"Baklunish": 80.0, "Oeridian": 12.0, "Suel": 8.0}}, "Half-Orc": {"chance": 5.0}, @@ -767,8 +719,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "iuz_and_horned_society": { - "aliases": ["Iuz", "Empire of Iuz", "Horned Society", "Molag", - "Dorakaa", "Bandit lands of Iuz", "Land of Iuz"], + "aliases": ["Iuz", "Empire of Iuz", "Horned Society", "Bandit lands of Iuz", "Land of Iuz"], "chances": { "Human": {"chance": 50.0, "subraces": {"Flan": 45.0, "Oeridian": 35.0, "Baklunish": 20.0}}, "Orc": {"chance": 16.0}, @@ -781,8 +732,7 @@ REGIONAL_OVERWRITES: dict = { }, }, "sea_barons_and_isles": { - "aliases": ["Sea Barons", "Lordship of Isles", "Asperdi", - "Duxchan", "Aerdi Sea", "Spindrift Isles", "Densac Gulf"], + "aliases": ["Sea Barons", "Lordship of Isles", "Aerdi Sea", "Spindrift Isles", "Densac Gulf"], "chances": { "Human": {"chance": 70.0, "subraces": {"Oeridian": 40.0, "Suel": 35.0, "Flan": 25.0}}, "Half-Orc": {"chance": 8.0}, @@ -923,10 +873,77 @@ PLANAR_OVERWRITES: dict = { } -# ─── Индекс алиасов для symspellpy ─────────────────────────────────────────── +# ─── Населенные пункты ─────────────────────────────────────────────────────── + +# Структура: Мир -> Регион -> [Список населенных пунктов] +SETTLEMENTS_OVERWRITES: dict = { + "Toril": { + "sword_coast_north": ["Luskan", "Neverwinter", "Port Llast", "Mirabar"], + "silver_marches": ["Silverymoon", "Sundabar", "Mithral Hall", "Everlund", "Citadel Adbar"], + "western_heartlands": ["Elturel", "Beregost", "Candlekeep", "Scornubel"], + "waterdeep": ["Waterdeep", "City of Splendors"], + "baldurs_gate": ["Baldur's Gate", "Gate", "Baldurs Gate"], + "cormyr": ["Suzail", "Marsember", "Arabel", "Wheloon"], + "amn": ["Athkatla", "Crimmor", "Eshpurta", "Purskul", "Trademeet"], + "tethyr": ["Zazesspur", "Darromar", "Myratma", "Ithmong", "Saradush", "Riatavin"], + "calimshan": ["Calimport", "Memnon", "Almraiven", "Keltar"], + "chult": ["Port Nyanzaru", "Mezro", "Omu"], + "unapproachable_east": ["Bezantur", "Two Stars", "Phsant", "Milvarune"], + "rashemen": ["Mulsantir", "Immilmar", "Urling", "Citadel Rashemar", "Mulptan"], + "thay": ["Eltabbar", "Tyraturos", "Amruthar", "Pyarados", "Surthay", "Nethjet"], + "underdark": ["Menzoberranzan", "Blingdenstone", "Gracklstugh", "Sshamath", "Ched Nasad"], + "moonsea": ["Zhentil Keep", "Melvaunt", "Hillsfar", "Mulmaster", "Phlan", "Thentia"], + "dalelands": ["Shadowdale", "Mistledale", "Deepingdale", "Battledale", "Daggerdale", "Archendale"], + "sembia": ["Selgaunt", "Ordulin", "Saerloon", "Yhaunn", "Daerlun", "Urmlaspyr"], + "vast": ["Ravens Bluff", "Tantras", "Calaunt", "Procampur", "Sarbreen"], + "vilhon_reach": ["Hlondeth", "Alaghon", "Arrabar", "Sespech", "Nimpeth"], + "old_empires": ["Skuld", "Messemprar", "Cimbar", "Soorenar"], + "icewind_dale": ["Ten Towns", "Bryn Shander", "Targos", "Lonelywood", "Easthaven", "Termalaine", "Caer-Dineval", "Caer-Konig", "Dougan's Hole", "Good Mead"], + "evermeet": ["Leuthilspar"], + "myth_drannor": ["Myth Drannor", "Semberholme", "Tangled Trees"], + "moonshae_isles": ["Callidyrr", "Corwell"], + "impiltur_and_bloodstone": ["Lyrabar", "Heliogabalus"], + "anauroch": ["Shade Enclave"], + "aglarond": ["Velprintalar", "Yeshelmaar", "Emmech"], + "lantan": ["Sambar", "Adaerglass"], + "sea_of_fallen_stars": ["Immurk's Hold"], + "sword_coast_central": ["Daggerford", "Secomber", "Boareskyr Bridge"], + "maztica": ["Helmsport", "Ulatos"], + "tashalar_and_shaar": ["Tashluta", "Sheirtalar"], + "nimbral": ["Cathtyr"], + }, + "Krynn": { + "abanasinia": ["Solace", "Haven", "Que-Shu"], + "solamnia": ["Palanthas", "Vingaard", "Kalaman"], + "silvanesti": ["Silvanost", "Silvamori"], + "qualinesti": ["Qualinost"], + "thorbardin": ["Zhakar"], + "neraka_and_taman_busuk": ["Sanction"], + "nordmaar_and_khur": ["North Keep", "Willik", "Wendle", "Kernaghan"], + "goodlund_and_balifor": ["Kendermore", "Port Balifor"], + + }, + "Oerth": { + "greyhawk_city": ["Greyhawk", "City of Greyhawk"], + "keoland_and_sheldomar": ["Niole Dra"], + "furyondy_and_shield_lands": ["Chendl", "Dyvers", "Willip"], + "pomarj_and_wild_coast": ["Highport", "Suderham"], + "great_kingdom_of_aerdy": ["Rauxes", "Rel Astra"], + "nyrond_and_urnsts": ["Rel Mord", "Womtham"], + "baklunish_west": ["Lopolla"], + "iuz_and_horned_society": ["Molag", "Dorakaa"], + "sea_barons_and_isles": ["Asperdi", "Duxchan"], + } +} + +# ─── Индекс алиасов и symspellpy ─────────────────────────────────────────── + +import os +import symspellpy _ARTICLES = {"the", "a", "an", "of", "and"} +# ----------------------------------------------- Core Dictionary Logic ------------------------------------------------------------ def _clean(text: str) -> str: """Нижний регистр, удаление артиклей и апострофов, схлопывание пробелов.""" @@ -934,13 +951,34 @@ def _clean(text: str) -> str: words = [w for w in text.split() if w not in _ARTICLES] return " ".join(words) +def _merge_settlements(): + """Вливает SETTLEMENTS_OVERWRITES в REGIONAL_OVERWRITES и создает плоский список.""" + world_settlements = {} + + for world_name, regions in SETTLEMENTS_OVERWRITES.items(): + world_settlements[world_name] = [] + + if world_name not in REGIONAL_OVERWRITES: + continue + + for region_key, settlements in regions.items(): + if region_key in REGIONAL_OVERWRITES[world_name]: + # Вливаем нас. пункты в aliases региона + REGIONAL_OVERWRITES[world_name][region_key]["aliases"].extend(settlements) + + # Формируем плоский список + world_settlements[world_name].extend(settlements) + + return world_settlements + +WORLD_SETTLEMENTS = _merge_settlements() + def _build_alias_index() -> tuple[dict, dict]: """ Возвращает два индекса: world_index: {"toril": "Toril", "avernus": "lower_planes_le", ...} region_index: {(scope, cleaned_alias): region_key} - scope — имя мира ("Toril") либо "__planes__". """ world_index: dict[str, str] = {} region_index: dict[tuple[str, str], str] = {} @@ -952,7 +990,6 @@ def _build_alias_index() -> tuple[dict, dict]: region_index[(world_name, _clean(alias))] = region_key for plane_key, data in PLANAR_OVERWRITES.items(): - # Планы адресуются и как world, и как region for alias in data["aliases"]: world_index[_clean(alias)] = plane_key region_index[("__planes__", _clean(alias))] = plane_key @@ -960,5 +997,46 @@ def _build_alias_index() -> tuple[dict, dict]: return world_index, region_index +WORLD_INDEX, REGION_INDEX = _build_alias_index() -WORLD_INDEX, REGION_INDEX = _build_alias_index() \ No newline at end of file + +# ----------------------------------------------- SymSpell Logic ------------------------------------------------------------ + +def _init_symspell_for_worlds(): + """Создает экземпляры SymSpell для каждого мира на основе плоского списка населенных пунктов.""" + symspells = {} + for world, settlements in WORLD_SETTLEMENTS.items(): + sym = symspellpy.SymSpell(max_dictionary_edit_distance=2, prefix_length=7) + for settlement in settlements: + # Добавляем в словарь оригинальное название и очищенное + clean_name = _clean(settlement) + if clean_name: + sym.create_dictionary_entry(clean_name, 1) + symspells[world] = sym + return symspells + +WORLD_SYMSPELLS = _init_symspell_for_worlds() + + +def match_location(world: str, location: str) -> str: + """ + Сопоставляет введенную локацию со словарем населенных пунктов мира. + Возвращает скорректированное название или оригинальное, если совпадений нет. + """ + world_key = world.strip().title() + if world_key not in WORLD_SYMSPELLS: + return location + + clean_loc = _clean(location) + sym = WORLD_SYMSPELLS[world_key] + + # Ищем исправления с максимальной дистанцией 2 + suggestions = sym.lookup(clean_loc, symspellpy.Verbosity.CLOSEST, max_edit_distance=2) + if suggestions: + best_match_clean = suggestions[0].term + # Ищем оригинальное написание по очищенному ключу + for settlement in WORLD_SETTLEMENTS.get(world_key, []): + if _clean(settlement) == best_match_clean: + return settlement + + return location diff --git a/generate_npc/tests/test_generate_npc.py b/generate_npc/tests/test_generate_npc.py new file mode 100644 index 0000000..9cee047 --- /dev/null +++ b/generate_npc/tests/test_generate_npc.py @@ -0,0 +1,60 @@ +import sys +from pathlib import Path + +scripts_path = Path(__file__).resolve().parent.parent.parent +if str(scripts_path) not in sys.path: + sys.path.insert(0, str(scripts_path)) + +npc_path = Path(__file__).resolve().parent.parent +if str(npc_path) not in sys.path: + sys.path.insert(0, str(npc_path)) + +from gen_npc import generate_npc + +# ----------------------------------------------- Тесты Генерации NPC ------------------------------------------------------------ + +def test_generate_npc_default(): + """ + Проверяет базовую генерацию NPC с пустыми ограничениями. + Убеждается, что возвращается непустая строка с результатом. + """ + result = generate_npc("тест", [{}]) + assert isinstance(result, str) + assert len(result) > 0 + assert "lvl 1" in result + +def test_generate_npc_with_region(): + """ + Проверяет генерацию NPC с указанием прайм-мира и конкретного региона. + Проверяет срабатывание региональных перезаписей. + """ + req = { + "world": "Toril", + "region": "Waterdeep", + "count": 2, + "level": 3 + } + result = generate_npc("тест", [req]) + assert isinstance(result, str) + assert "lvl 3" in result + assert "#2 —" in result + +def test_generate_npc_planar(): + """ + Проверяет генерацию NPC для планарных миров (без региона). + Гарантирует корректную маршрутизацию к планарным шансам. + """ + req = { + "world": "lower_planes_le", + "count": 1, + "level": 5 + } + result = generate_npc("тест", [req]) + assert isinstance(result, str) + assert "lvl 5" in result + +if __name__ == "__main__": + test_generate_npc_default() + test_generate_npc_with_region() + test_generate_npc_planar() + print("Все тесты generate_npc успешно пройдены!") diff --git a/generate_npc/tests/test_regions.py b/generate_npc/tests/test_regions.py new file mode 100644 index 0000000..57f20cd --- /dev/null +++ b/generate_npc/tests/test_regions.py @@ -0,0 +1,45 @@ +import sys +from pathlib import Path + +# Добавляем родительскую папку в sys.path для импорта +scripts_path = Path(__file__).resolve().parent.parent +if str(scripts_path) not in sys.path: + sys.path.insert(0, str(scripts_path)) + +from regions import match_location + +# ----------------------------------------------- Тесты Fuzzy Matcher ------------------------------------------------------------ + +def test_match_exact(): + """ + Проверяет точное совпадение локации. + """ + assert match_location("Toril", "Waterdeep") == "Waterdeep" + assert match_location("Toril", "Neverwinter") == "Neverwinter" + +def test_match_fuzzy(): + """ + Проверяет коррекцию опечаток (fuzzy matching). + """ + assert match_location("Toril", "Watterdeep") == "Waterdeep" + assert match_location("Toril", "Neverwintar") == "Neverwinter" + assert match_location("Toril", "baldur gate") == "Baldur's Gate" + +def test_match_unknown_world(): + """ + Проверяет поведение при передаче неизвестного мира. + """ + assert match_location("Unknown", "Waterdeep") == "Waterdeep" + +def test_match_no_correction_needed(): + """ + Проверяет поведение для локаций, которых нет в словаре. + """ + assert match_location("Toril", "Some Random City") == "Some Random City" + +if __name__ == "__main__": + test_match_exact() + test_match_fuzzy() + test_match_unknown_world() + test_match_no_correction_needed() + print("Все тесты match_location успешно пройдены!") diff --git a/generate_trap/generate_trap.py b/generate_trap/gen_trap.py similarity index 100% rename from generate_trap/generate_trap.py rename to generate_trap/gen_trap.py diff --git a/generate_treasure/generate_treasure.py b/generate_treasure/gen_treasure.py similarity index 100% rename from generate_treasure/generate_treasure.py rename to generate_treasure/gen_treasure.py diff --git a/requirements.txt b/requirements.txt index 82c4bf4..f213539 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ PyYaml -pathspec \ No newline at end of file +pathspec +symspellpy # для тестов diff --git a/weather/weather.py b/weather/gen_weather.py similarity index 100% rename from weather/weather.py rename to weather/gen_weather.py