From 0ab91a6b94259d25633dc23d6396abfc47b21f70 Mon Sep 17 00:00:00 2001 From: dimitrievgs Date: Wed, 29 Jul 2026 22:42:29 +0300 Subject: [PATCH] refactor(vault_utils): replace search_simple with search_by_filename and clean unused imports --- TASKS.md | 12 +++++++ faction_turns/faction_utils.py | 3 +- faction_turns/propose_faction_turn.py | 10 +++--- faction_turns/resolve_faction_turn.py | 44 ++++++++++++++++------- faction_turns/tests/test_repair_boi.py | 48 +++++++++++++++++++++++++ faction_turns/tests/test_vault_utils.py | 6 ++-- faction_turns/tests/verify_factions.py | 6 ++-- vault_utils.py | 25 ++++++++++--- 8 files changed, 127 insertions(+), 27 deletions(-) create mode 100644 faction_turns/tests/test_repair_boi.py diff --git a/TASKS.md b/TASKS.md index ef88309..164f103 100644 --- a/TASKS.md +++ b/TASKS.md @@ -22,3 +22,15 @@ - [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` diff --git a/faction_turns/faction_utils.py b/faction_turns/faction_utils.py index 6059a02..379693c 100644 --- a/faction_turns/faction_utils.py +++ b/faction_turns/faction_utils.py @@ -38,9 +38,10 @@ scripts_path = Path(__file__).resolve().parent.parent if str(scripts_path) not in sys.path: sys.path.insert(0, str(scripts_path)) +# Важно! не удалять неиспользуемые импорты - они используются в propose/resolve_faction_trun from vault_utils import ( get_file, - search_simple, + search_by_filename, search_jsonlogic, list_directory, get_recent_files, diff --git a/faction_turns/propose_faction_turn.py b/faction_turns/propose_faction_turn.py index 396462a..394d9c6 100644 --- a/faction_turns/propose_faction_turn.py +++ b/faction_turns/propose_faction_turn.py @@ -33,7 +33,6 @@ except ImportError: from faction_utils import ( WWN_MILES_PER_TURN, get_travel_distance, - vault_query, get_all_factions, get_factions_in_location, get_assets_in_location, @@ -42,7 +41,7 @@ from faction_utils import ( roll_dice, roll_attribute_check, are_within_one_move, - write_faction, + write_faction, ) from wwn_assets_data import ( get_available_assets, @@ -76,7 +75,10 @@ class FactionTurnProposalRequest(TypedDict, total=False): Поля: location_wikilink: Имя локации на русском языке в формате Obsidian wikilink. Обязательное поле. - Пример: "[[Уотердип]]", "[[Невервинтер]]", название локации на русском. + КРИТИЧЕСКИ ВАЖНО: Копируй название буква в букву из запроса, + СТРОГО в именительном падеже. ЗАПРЕЩЕНО склонять, менять + окончания, переводить или придумывать буквы. + Пример: "[[Даггерфорд]]", "[[Калимшан]]", название локации на русском. factions_folder: Путь к папке с файлами фракций внутри vault. По умолчанию: "Фракции". @@ -565,7 +567,7 @@ def _format_faction_section( return ( f"### {idx}. `{name}` | Initiative: {initiative} | {state_label.upper()}\n" - f"**Файл:** `{name}.md`\n" # Добавьте эту строку + f"faction_file=={name}.md\n" # Добавьте эту строку f"**Для resolve:** faction_file: \"{name}\"" f"**Статы:** {status_str}" f"{tags_str}" diff --git a/faction_turns/resolve_faction_turn.py b/faction_turns/resolve_faction_turn.py index 3af0267..758fd2e 100644 --- a/faction_turns/resolve_faction_turn.py +++ b/faction_turns/resolve_faction_turn.py @@ -46,7 +46,7 @@ except ImportError: from typing import TypedDict from faction_utils import ( - vault_query, + get_file, get_all_factions, write_faction, create_event_file, @@ -194,8 +194,9 @@ def _load_faction_file(faction_file: str, factions_folder: str) -> tuple[dict, s if not path: path = f"{factions_folder}/{faction_file}.md" - data = vault_query("get_frontmatter", {"path": path}) - if "error" in data: + try: + data = get_file(path) + except RuntimeError: raise ValueError( f"Фракция не найдена: '{faction_file}'. " f"Obsidian не смог найти путь автоматически, а по пути '{path}' файла нет." @@ -286,16 +287,21 @@ def _apply_damage_to_boi( def _make_event_filename(game_date: str, faction_file: str, action_type: str) -> str: """ - Генерирует имя файла события. + Генерирует читаемое имя файла события. - Формат: YYYY-MM-DD_{faction}_{action}.md - Если game_date не в ISO формате — использует реальную дату. + Формат: Date_Faction_Action.md """ - safe_date = re.sub(r'[^\d\-]', '-', game_date)[:10] - if len(safe_date) < 10: - safe_date = date.today().isoformat() - safe_faction = re.sub(r'[^a-zA-Z0-9_]', '_', faction_file) - safe_action = re.sub(r'[^a-zA-Z0-9_]', '_', action_type) + # Извлекаем все буквы и цифры из даты, объединяем через дефис (например: 15-Mirtul-1492-DR) + safe_date_parts = re.findall(r'[a-zA-Zа-яА-Я0-9]+', game_date) + safe_date = "-".join(safe_date_parts) if safe_date_parts else date.today().isoformat() + + # Заменяем все не-буквенно-цифровые символы на подчеркивания + safe_faction = re.sub(r'[^a-zA-Zа-яА-Я0-9]', '_', faction_file) + safe_faction = re.sub(r'_+', '_', safe_faction).strip('_') + + safe_action = re.sub(r'[^a-zA-Zа-яА-Я0-9]', '_', action_type) + safe_action = re.sub(r'_+', '_', safe_action).strip('_') + return f"{safe_date}_{safe_faction}_{safe_action}.md" @@ -726,7 +732,21 @@ def _resolve_repair_asset( result["hp_after"] = faction_fm["hp"] result["treasure_spent"] = cost else: - asset = _find_asset_in_faction(faction_fm, repair_target) + # Проверяем, является ли цель Base of Influence + asset = None + if repair_target.startswith("Base of Influence"): + location_from_action = action.get("location", "") + if location_from_action: + asset = _find_boi_in_faction(faction_fm, location_from_action) + + if not asset: + for a in faction_fm.get("assets", []): + if a.get("is_base_of_influence") and (repair_target in a.get("name", "") or a.get("name", "").startswith("Base of Influence")): + asset = a + break + else: + asset = _find_asset_in_faction(faction_fm, repair_target) + if not asset: result["error"] = f"Актив '{repair_target}' не найден" return result diff --git a/faction_turns/tests/test_repair_boi.py b/faction_turns/tests/test_repair_boi.py new file mode 100644 index 0000000..ade9403 --- /dev/null +++ b/faction_turns/tests/test_repair_boi.py @@ -0,0 +1,48 @@ +import sys +from pathlib import Path + +# Добавляем путь к scripts и faction_turns +scripts_path = Path(__file__).resolve().parent.parent.parent +faction_turns_path = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(scripts_path)) +sys.path.insert(0, str(faction_turns_path)) + +from resolve_faction_turn import _resolve_repair_asset + +def test_repair_boi_powerful(): + print("--- Тест Repair Asset для мощной фракции ---") + + faction_fm = { + "name": "Powerful Faction", + "treasure": 10, + "force": 6, + "assets": [ + { + "name": "Base of Influence — [[Лускан]]", + "is_base_of_influence": True, + "hp": 5, + "hp_max": 20, + "location": "[[Лускан]]" + } + ] + } + + action = { + "faction_file": "Powerful_Faction", + "action_type": "Repair Asset", + "location": "[[Лускан]]", + "repair_asset": "Base of Influence" + } + + result = _resolve_repair_asset(action, faction_fm, 1) + + print(f"Результат: {result}") + assert "error" not in result, f"Ошибка: {result.get('error')}" + + assert result["healed"] == 3, f"Ожидали лечение на 3 (ceil(Force 6 / 2)), получили {result['healed']}" + assert result["hp_after"] == 8, f"Ожидали HP 8, получили {result['hp_after']}" + + print("\nТест мощной фракции успешно пройден!") + +if __name__ == "__main__": + test_repair_boi_powerful() diff --git a/faction_turns/tests/test_vault_utils.py b/faction_turns/tests/test_vault_utils.py index 776f099..3c5da89 100644 --- a/faction_turns/tests/test_vault_utils.py +++ b/faction_turns/tests/test_vault_utils.py @@ -11,7 +11,7 @@ import os sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../..'))) from vault_utils import ( - get_file, write_file, create_file, patch_frontmatter, search_simple, list_directory, + get_file, write_file, create_file, patch_frontmatter, search_by_filename, list_directory, normalize_wikilink, resolve_wikilink ) @@ -36,9 +36,9 @@ def test_list_directory(): def test_search_simple(): """Проверяет простой поиск по имени файла (POST).""" - result = search_simple("Test_Vault_Utils") + result = search_by_filename("Test_Vault_Utils") assert len(result) > 0 - assert result[0]["filename"] == TEST_FILE_PATH + assert result[0] == TEST_FILE_PATH def test_get_file(): """Проверяет чтение контента и парсинг frontmatter (GET).""" diff --git a/faction_turns/tests/verify_factions.py b/faction_turns/tests/verify_factions.py index 372be93..3453eb7 100644 --- a/faction_turns/tests/verify_factions.py +++ b/faction_turns/tests/verify_factions.py @@ -3,14 +3,14 @@ from pathlib import Path # Добавляем путь к scripts, чтобы импортировать vault_utils sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent)) -from vault_utils import vault_query +from faction_utils import get_all_factions # Проверяем работу get_all_factions folder = "Фракции/Tests" print(f"--- Проверка загрузки для папки: {folder} ---") -# Прямой вызов get_all_factions через vault_query -result = vault_query("get_all_factions", {"folder": folder}) +# Вызов функции get_all_factions +result = get_all_factions(folder) print("Результат:", result) diff --git a/vault_utils.py b/vault_utils.py index 20f9e1b..72c99ec 100644 --- a/vault_utils.py +++ b/vault_utils.py @@ -86,7 +86,24 @@ def patch_frontmatter(path: str, target: str, value: Any, operation: str = "repl resp = requests.patch(f"{BASE_URL}/vault/{encoded_path}", headers=headers, data=val.encode('utf-8'), **_get_req_kwargs()) return resp.status_code in [200, 204] -def search_simple(query: str) -> list[dict]: +def search_by_filename(filename_query: str) -> list[str]: + """ + Выполняет поиск в Obsidian строго по названию файла (или части пути). + Возвращает список путей к найденным файлам. + """ + # Формируем JsonLogic запрос, который вернет true, если filename_query есть в пути + query = { + "glob": [f"*{filename_query}*.md", {"var": "path"}] + } + + # Используем вашу готовую функцию для отправки JsonLogic запроса + raw_results = search_jsonlogic(query) + + # Эндпоинт возвращает список словарей вида [{"filename": "...", "result": true}] + # Отфильтруем и вернем только пути (имена файлов) + return [item.get("filename") for item in raw_results if item.get("filename")] + +'''def search_simple(query: str) -> list[dict]: """Выполняет простой текстовый поиск в Obsidian.""" resp = requests.post( f"{BASE_URL}/search/simple/", @@ -96,7 +113,7 @@ def search_simple(query: str) -> list[dict]: ) if resp.status_code == 200: return resp.json() - return [] + return []''' def search_jsonlogic(query: dict) -> list[dict]: """Выполняет поиск по правилам JsonLogic в Obsidian.""" @@ -130,8 +147,8 @@ def normalize_wikilink(s: str) -> str: def resolve_wikilink(name: str) -> Optional[str]: """Ищет путь к файлу по имени wikilink через простой поиск.""" original_clean = re.sub(r'^\[\[|\]\]$', '', name).split('|')[0].strip() - results = search_simple(original_clean) - return results[0].get("filename") if results else None + results = search_by_filename(original_clean) + return results[0] if results else None def get_recent_files(