/
github_better
/
GenAI
Обзор
Документация
Войти
/
github_better
/
GenAI
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
Inventory.py
769 строк
30 KB
github_better
New
22 дек 2025, 08:52
22 дек 2025, 08:52
b68d8c4
Код
Авторство
О чём код?
import json import logging import re import Lib.Text_formatting as Text_formatting import Lib.Translate_text as Translate_text import Database.Database as Database from Lib.Debugger import debug_print from Lib.Translate_text import translate_text logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def remove_items_by_name(inv_data: list | str, drop_list: str | list) -> list: debug_print(logger) """ Удаляет предметы из инвентаря по их имени и количеству. Работает устойчиво даже если элементы инвентаря или списка выброса — строки. :param inv_data: инвентарь :param drop_list: список предметов, которые нужно удалить (+каличество) :return: обновленный список без удаленных предметов """ # --- нормализуем инвентарь --- if isinstance(inv_data, str): try: inv = json.loads(inv_data.replace("'", '"')) except Exception: inv = [] else: inv = inv_data or [] if not isinstance(inv, list): inv = [] # --- нормализуем список выброса --- drops = [] if isinstance(drop_list, str): try: parsed = json.loads(re.sub(r"[^{}\[\],:0-9A-Za-zА-Яа-я\"' ]", "", drop_list)) if isinstance(parsed, dict): drops = [parsed] elif isinstance(parsed, list): drops = parsed except Exception: matches = re.findall( r'["\']?name["\']?\s*[:=]\s*["\']([^"\']+)["\'].*?["\']?count["\']?\s*[:=]\s*([0-9]+)', drop_list) for name, cnt in matches: drops.append({"name": name.strip(), "count": int(cnt)}) elif isinstance(drop_list, list): drops = drop_list if not drops: return inv # --- удаляем предметы --- for drop in drops: name = str(drop.get("name", "")).strip() count_to_remove = int(drop.get("count", 1)) if not name or count_to_remove <= 0: continue for raw_obj in inv[:]: # копия списка obj = raw_obj if isinstance(raw_obj, str): try: obj = json.loads(raw_obj.replace("'", '"')) except Exception: continue if not isinstance(obj, dict): continue if name.lower() == str(obj.get("name", "")).lower(): current_count = int(obj.get("count", 1)) if count_to_remove >= current_count: inv.remove(raw_obj) else: obj["count"] = current_count - count_to_remove # если элемент был строкой — заменим его на dict if isinstance(raw_obj, str): inv[inv.index(raw_obj)] = obj break return inv async def try_add_to_inventory(sess, new_objects, message): debug_print(logger) """ Пытается добавить новые предметы в инвентарь с учетом лимитов. Безопасно работает, даже если в инвентаре есть строки. :param sess: текущая сессия игрока :param new_objects: новые объекты, которые добавляются в инвентарь :param message: последнее сообщение юзера, чтобы на него можно было ответить в чате """ # --- нормализуем вход --- if isinstance(new_objects, str): try: objects = json.loads(new_objects.replace("'", '"')) except Exception: await message.reply("❌ Ошибка при разборе предметов.") return else: objects = new_objects if isinstance(objects, dict): objects = [objects] not_taken = [] taken_items = [] max_weight, max_slots = Text_formatting.parse_max_weight_and_slots(sess.current_stats) # --- нормализуем текущий инвентарь --- normalized_inv = [] for raw_obj in (sess.current_inventory or []): if isinstance(raw_obj, str): try: parsed = json.loads(raw_obj.replace("'", '"')) if isinstance(parsed, dict): normalized_inv.append(parsed) except Exception: continue elif isinstance(raw_obj, dict): normalized_inv.append(raw_obj) sess.current_inventory = normalized_inv for obj in objects: if not isinstance(obj, dict): continue weight = float(obj.get("weight", 0)) slots = int(obj.get("slots", 0)) count = int(obj.get("count", 1)) name = obj.get("name", "неизвестный предмет") available_by_weight = int((max_weight - sess.current_weight) // weight) if weight > 0 else count available_by_slots = int((max_slots - sess.current_slots) // slots) if slots > 0 else count can_take = min(count, available_by_weight, available_by_slots) if can_take > 0: taken_obj = obj.copy() taken_obj["count"] = can_take sess.current_inventory = Text_formatting.add_new_obj_to_inv( sess.current_inventory, json.dumps(taken_obj, ensure_ascii=False) ) sess.current_weight += weight * can_take sess.current_slots += slots * can_take taken_items.append(f"{name} x{can_take}") if can_take < count: not_taken.append(f"{name} x{count - can_take}") if taken_items: text_tr = Translate_text.translate_text("Added to inventory", "en", sess.lang) await message.reply(f"👜 {text_tr}: {', '.join(taken_items)}") if not_taken: text_tr = Translate_text.translate_text("I can't put it in the inventory.", "en", sess.lang) await message.reply(f"❌ {text_tr}: {', '.join(not_taken)}") def stack_or_add(inventory: list | str | None, new_obj: dict | str) -> list | str: debug_print(logger) """ Складывает одинаковые предметы или добавляет новый предмет в инвентарь. Безопасна для случаев, когда inventory или new_obj — строка (JSON или сырой текст). :param inventory: список предметов или JSON-строка :param new_obj: новый предмет (dict или JSON-строка) :return: обновлённый список или строка (в зависимости от исходного типа) """ # --- нормализуем new_obj --- if isinstance(new_obj, str): try: parsed = json.loads(new_obj.replace("'", '"')) if isinstance(parsed, dict): new_obj = parsed else: # строка не содержит словарь — оставляем как есть return inventory except Exception: return inventory # --- если inventory — строка, пытаемся превратить её в список --- inv_was_str = False if isinstance(inventory, str): inv_was_str = True try: parsed = json.loads(inventory.replace("'", '"')) if isinstance(parsed, list): inventory = parsed elif isinstance(parsed, dict): inventory = [parsed] else: # не получилось — создаём пустой список inventory = [] except Exception: # невалидная строка — создаём список с одним элементом (позже конвертируем обратно) inventory = [] # если после всего это не список — создаём новый if not isinstance(inventory, list): inventory = [] # --- складываем одинаковые предметы --- for i, raw_obj in enumerate(inventory): obj = raw_obj if isinstance(raw_obj, str): try: parsed = json.loads(raw_obj.replace("'", '"')) if isinstance(parsed, dict): obj = parsed inventory[i] = obj except Exception: continue if not isinstance(obj, dict): continue if ( obj.get("name") == new_obj.get("name") and obj.get("weight") == new_obj.get("weight") and obj.get("slots") == new_obj.get("slots") ): obj["count"] = obj.get("count", 1) + new_obj.get("count", 1) # если исходник был строкой — возвращаем строку return json.dumps(inventory, ensure_ascii=False) if inv_was_str else inventory inventory.append(new_obj) # --- если изначально inventory был строкой — вернуть строку, а не список --- if inv_was_str: try: return json.dumps(inventory, ensure_ascii=False) except Exception: # как крайний случай — "ручное" сложение строк, чтобы выглядело как JSON-массив inv_str = inventory if isinstance(inventory, str) else str(inventory) new_str = json.dumps(new_obj, ensure_ascii=False) if inv_str.strip().startswith('[') and inv_str.strip().endswith(']'): inv_str = inv_str.strip()[:-1] + f", {new_str}]" else: inv_str = f"[{new_str}]" return inv_str return inventory def normalize_new_objects(new_objects) -> list[dict]: debug_print(logger) """ Приводит результат AI_request.take_inventory к списку словарей. :param new_objects: объекты различных типов (dict, list, str, JSON) :return: нормализованный список словарей """ if not new_objects: return [] if isinstance(new_objects, dict): return [new_objects] if isinstance(new_objects, list): return new_objects if isinstance(new_objects, str): try: obj = json.loads(new_objects.replace("'", '"')) if isinstance(obj, dict): return [obj] if isinstance(obj, list): return obj except Exception: return [] return [] def recalc_inventory_stats(inventory: list) -> tuple[float, int]: debug_print(logger) """ Пересчитывает общий вес и количество занятых слотов для инвентаря. Защищённая версия: элементы инвентаря могут быть dict, json-строкой или чем угодно. Встречая строку — попытаемся её распарсить; если не получится — пропустим элемент. :param inventory: список предметов в инвентаре :return: кортеж (общий_вес, занятые_слоты) """ total_weight = 0.0 total_slots = 0 if not inventory: return total_weight, total_slots for raw_obj in inventory: obj = raw_obj # Если строка — пытаемся распарсить JSON if isinstance(raw_obj, str): try: parsed = json.loads(raw_obj.replace("'", '"')) # если пришла не структура, сохраняем parsed как есть obj = parsed except Exception: # строка не JSON — пропускаем continue # Если по-прежнему не словарь — пропускаем (без ошибки) if not isinstance(obj, dict): continue # безопасно извлекаем числовые поля try: count = int(obj.get("count", 1) or 1) except Exception: count = 1 try: weight = float(obj.get("weight", 0) or 0.0) except Exception: weight = 0.0 try: slots = int(obj.get("slots", 0) or 0) except Exception: slots = 0 # подсчёт total_weight += weight * max(count, 0) total_slots += (slots if count > 0 else 0) # Округлим вес до 2 знаков для стабильности (поведение прежнее можно сохранить) return round(total_weight, 2), int(total_slots) def pretty_inv(inv: list | str, sess) -> str: debug_print(logger) """ Красиво форматирует инвентарь. Устойчивая версия: поддерживает inv как list, как JSON-string (весь массив) и элементы как dict или JSON-string или простые строковые описания ("Sword x1"). :param inv: текущий инвентарь юзера :param sess: сессия юзера :return: отформатированный, красивый инвентарь """ # --- Нормализуем inv: если это строка, пробуем распарсить весь массив --- normalized = None if isinstance(inv, str): s = inv.strip() if not s: normalized = [] else: # пробуем корректный JSON (поддержка одинарных кавычек) try: parsed = json.loads(s.replace("'", '"')) if isinstance(parsed, list): normalized = parsed elif isinstance(parsed, dict): normalized = [parsed] else: normalized = [s] # неструктурированная строка — считаем одним элементом except Exception: # как запасной вариант: ищем JSON-объекты внутри строки try: # попытка извлечь все {...} подряд и распарсить их objs = re.findall(r'\{[^}]+\}', s) parsed_objs = [] for o in objs: try: parsed_o = json.loads(o.replace("'", '"')) if isinstance(parsed_o, dict): parsed_objs.append(parsed_o) except Exception: continue if parsed_objs: normalized = parsed_objs else: normalized = [s] except Exception: normalized = [s] else: normalized = inv or [] if not isinstance(normalized, list): normalized = [normalized] # Пустой инвентарь if not normalized: return translate_text("Inventory is empty.", "en", sess.lang) max_weight, max_slots = Text_formatting.parse_max_weight_and_slots(sess.current_stats) header = ( f"{translate_text('Slots', 'en', sess.lang)}: {sess.current_slots}/{max_slots}\n" f"{translate_text('Weight', 'en', sess.lang)}: {round(sess.current_weight, 2)}/{max_weight}\n\n" ) lines = [] for i, raw_obj in enumerate(normalized, start=1): obj = None # если элемент — строка, пробуем разобрать if isinstance(raw_obj, str): s = raw_obj.strip() # сначала — JSON объект try: parsed = json.loads(s.replace("'", '"')) if isinstance(parsed, dict): obj = parsed elif isinstance(parsed, list): # если внутри список — берём первый элемент как представление if parsed: obj = parsed[0] if isinstance(parsed[0], dict) else {"name": str(parsed[0]), "count": 1} else: obj = {"name": s, "count": 1} else: obj = {"name": s, "count": 1} except Exception: # попробуем формат "Name xN" m = re.match(r'^(.*?)(?:\s+x\s*|\s+x|\s*\u00D7\s*|\s*\*?\s*)(\d+)$', s, flags=re.IGNORECASE) if not m: # ещё вариант "Name xN" с символом х или крестик m = re.match(r'^(.*)\s+[x×]\s*(\d+)$', s, flags=re.IGNORECASE) if m: name_part = m.group(1).strip() try: cnt = int(m.group(2)) except Exception: cnt = 1 obj = {"name": name_part or s, "count": cnt, "weight": 0, "slots": 0} else: # просто используем всю строку как имя obj = {"name": s, "count": 1, "weight": 0, "slots": 0} elif isinstance(raw_obj, dict): obj = raw_obj else: # неожиданный тип — попытка привести к строке-имени try: obj = {"name": str(raw_obj), "count": 1, "weight": 0, "slots": 0} except Exception: continue # если по какой-то причине obj не dict — пропускаем if not isinstance(obj, dict): continue # безопасность извлечения полей name = obj.get("name", "???") try: count = int(obj.get("count", 1) or 1) except Exception: count = 1 try: weight = obj.get("weight", 0) weight = float(weight) if weight is not None else 0 except Exception: weight = 0 try: slots = obj.get("slots", 0) slots = int(slots) if slots is not None else 0 except Exception: slots = 0 display_count = 1 if count == -1 else count lines.append( f"{i}) {name} x{display_count} " f"[{translate_text('Weight', 'en', sess.lang)} {weight}, " f"{translate_text('Slots', 'en', sess.lang)} {slots}]" ) # Если lines пустые — вернуть понятный текст if not lines: return header + translate_text("Inventory is empty.", "en", sess.lang) return header + "\n".join(lines) def add_information_about_user_drop_inv_in_hist(drop_objects: str, current_location: str): debug_print(logger) """ Формирует запись в историю о выброшенных игроком предметах. :param drop_objects: строка с информацией о выброшенных предметах :param current_location: текущая локация персонажа :return: словарь с ролью system и описанием действия """ if not drop_objects or not isinstance(drop_objects, str): return "" # Пытаемся вытащить имена предметов и количество pattern = r'["\']?name["\']?\s*[:=]\s*["\']([^"\']+)["\'](?:.*?["\']?count["\']?\s*[:=]\s*([0-9]+))?' matches = re.findall(pattern, drop_objects, flags=re.IGNORECASE) items = [] for name, cnt in matches: name = name.strip() if not name: continue if cnt and cnt.isdigit() and int(cnt) > 1: items.append(f"{name} ×{cnt}") else: items.append(name) # fallback — если ни одного совпадения не нашли if not items: clean_str = re.sub(r'[^A-Za-zА-Яа-я0-9ёЁ\s,.-]', '', drop_objects) words = clean_str.strip() if words: items.append(words) if not items: return "" items_str = ", ".join(items) loc_phrase = f" near {current_location}" if current_location else "" text = f"The hero quietly left {items_str}{loc_phrase}, not making much of it." return {'role': 'system', 'content': text} def parse_user_items(user_string: str) -> list[dict]: debug_print(logger) """ Разбирает строку, введённую пользователем, в список предметов. Формат ввода (через пробелы, а не запятые): name count weight slots; name2 count2 weight2 slots2 Пример: "Long sword 1 5 1; Gold coins 5 0.1 0" :param user_string: строка юзера :return: список словарей (словарь это параметры предмета) """ items = [] if not user_string: return items # Разбиваем по предметам через точку с запятой raw_items = [i.strip() for i in user_string.split(";") if i.strip()] for raw in raw_items: parts = raw.split() if len(parts) < 4: # если данных меньше 4 — пропускаем continue # последние три — это количество, вес, слоты try: count = int(parts[-3]) except ValueError: count = 1 try: weight = float(parts[-2]) except ValueError: weight = 0.0 try: slots = int(parts[-1]) except ValueError: slots = 0 # всё, что до них — название name = " ".join(parts[:-3]) if not name: continue items.append({ "name": name, "count": count, "weight": weight, "slots": slots }) return items def parse_drop_input(user_input: str) -> list: debug_print(logger) """ Парсит ввод пользователя для выбрасывания предметов. Формат: "1 3; 2; 5 1" Означает: предмет 1 выбросить 3 шт, предмет 2 выбросить весь, предмет 5 выбросить 1 шт. :param user_input: строка ввода от пользователя :return: список кортежей [(item_index, quantity_to_drop), ...] quantity_to_drop = None означает выбросить все :param user_input: строка с параметрами предметов :return: список выбрасываемых предметов """ if not user_input or not user_input.strip(): return [] result = [] parts = user_input.split(";") for part in parts: part = part.strip() if not part: continue tokens = part.split() if len(tokens) == 0: continue elif len(tokens) == 1: # Только номер предмета — выбросить весь стек try: item_idx = int(tokens[0]) result.append((item_idx, None)) except ValueError: continue elif len(tokens) == 2: # Номер предмета и количество try: item_idx = int(tokens[0]) quantity = int(tokens[1]) if quantity <= 0: continue result.append((item_idx, quantity)) except ValueError: continue else: # Некорректный формат этой части — игнорируем continue return result def format_dropped_items_message(dropped_items: list, lang: str) -> str: debug_print(logger) """ Форматирует сообщение о выброшенных предметах. Делает их в красивом формате. :param dropped_items: список выброшенных предметов. :param lang: язык на котором выводится отформатированный :return: красивая строка """ if not dropped_items: return "" lines = [translate_text("You threw away:", "en", lang)] for item in dropped_items: name = item.get("name", "Unknown") count = item.get("count", 1) lines.append(f"• {name} x{count}") return "\n".join(lines) async def drop_items_batch(sess, drop_instructions: list) -> list: debug_print(logger) """ Выбрасывает предметы из инвентаря по списку инструкций. :param sess: сессия пользователя :param drop_instructions: список [(item_index, quantity), ...] quantity=None означает выбросить весь стек :return: список выброшенных предметов [{"name": ..., "count": ..., "weight": ..., "slots": ...}, ...] """ if not drop_instructions: return [] dropped_items = [] inventory = sess.current_inventory # Сортируем по убыванию индекса, чтобы удаление не сбивало индексы drop_instructions = sorted(drop_instructions, key=lambda x: x[0], reverse=True) for item_idx, quantity_to_drop in drop_instructions: # Проверяем валидность индекса if item_idx < 1 or item_idx > len(inventory): continue # Индекс в списке (с 0) idx = item_idx - 1 item = inventory[idx] # Нормализуем количество try: current_count = int(item.get("count", 1)) except Exception: current_count = 1 item_name = item.get("name", "Unknown") # Защита от деления на ноль if current_count <= 0: current_count = 1 total_weight = float(item.get("weight", 0.0)) if item.get("weight") is not None else 0.0 total_slots = int(item.get("slots", 1)) if item.get("slots") is not None else 1 weight_per_one = total_weight / current_count slots_per_one = total_slots / current_count # Если quantity_to_drop не указан или >= текущего количества — выбрасываем весь стек if quantity_to_drop is None or quantity_to_drop >= current_count: dropped_count = current_count dropped_weight = total_weight dropped_slots = total_slots # Удаляем предмет полностью inventory.pop(idx) dropped_items.append({ "name": item_name, "count": dropped_count, "weight": dropped_weight, "slots": dropped_slots }) else: # Выбрасываем только часть dropped_count = quantity_to_drop dropped_weight = weight_per_one * dropped_count dropped_slots = slots_per_one * dropped_count # Уменьшаем количество в инвентаре new_count = current_count - dropped_count item["count"] = new_count item["weight"] = weight_per_one * new_count item["slots"] = slots_per_one * new_count dropped_items.append({ "name": item_name, "count": dropped_count, "weight": dropped_weight, "slots": dropped_slots }) # --- Пересчитываем общий вес и слоты инвентаря --- sess.current_weight, sess.current_slots = recalc_inventory_stats(sess.current_inventory) # --- Сохраняем в БД (синхронно, без использования функций из Bot.py) --- try: Database.give_value( "histories", {"history_id": sess.history_id}, "current_inventory", json.dumps(sess.current_inventory, ensure_ascii=False) ) Database.give_value( "histories", {"history_id": sess.history_id}, "current_weight", float(sess.current_weight) ) Database.give_value( "histories", {"history_id": sess.history_id}, "current_slots", int(sess.current_slots) ) except Exception as e: debug_print(logger, f"Error saving inventory after drop: {e}") return dropped_items