refactor: separate settlements in regions.py, integrate symspellpy fuzzy location matching into WWN faction tools, and add tests

This commit is contained in:
dimitrievgs 2026-07-30 00:23:40 +03:00
parent 0ab91a6b94
commit f819946d1c
14 changed files with 427 additions and 152 deletions

View File

@ -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/ # Управление временем кампании

View File

@ -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`).

View File

@ -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):
Допустимы: 120.
По умолчанию: 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"]

View File

@ -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", ""))

View File

@ -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
# Множество планов существования (не прайм-миры).

View File

@ -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 успешно пройдены!")

View File

@ -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}

View File

@ -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()
# ----------------------------------------------- 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

View File

@ -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 успешно пройдены!")

View File

@ -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 успешно пройдены!")

View File

@ -1,2 +1,3 @@
PyYaml
pathspec
symspellpy # для тестов