Files
dodep-simulator/backend.py
T

971 lines
37 KiB
Python

# main.py
from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel, field_validator, validator
from typing import List, Optional, Dict, Any
import json
from pathlib import Path
from collections import Counter, defaultdict
import random
import sys
from tqdm import tqdm
app = FastAPI(
title="CS2 Trade-Up Contract Simulator API",
description="Симуляция контрактов 10→1 по механике CS2",
version="1.0.0"
)
DATA_FILE = Path("skins.json")
CUSTOM_DATA_FILE = Path("custom_items.json")
# ─── Загрузка кастомных предметов ─────────────────────────────────────────
def load_custom_items() -> List[dict]:
"""Загружает кастомные предметы из отдельного файла"""
custom_items = []
if CUSTOM_DATA_FILE.exists():
try:
with CUSTOM_DATA_FILE.open("r", encoding="utf-8") as f:
custom_items = json.load(f)
print(f"Загружено {len(custom_items)} кастомных предметов")
except Exception as e:
print(f"Ошибка загрузки кастомных предметов: {e}")
else:
# Создаем пример файла с кастомными предметами
example_custom = [
{
"market_hash_name": "★ Dildo | Fade (Factory New)",
"weapon": "Dildo",
"pattern": "Fade",
"rarity": "Covert",
"collection": "",
"type": "Normal",
"wear": "Factory New",
"wear_range": [0.00, 0.07],
"mid_float_used": 0.01,
"doppler_phase": None,
"price_usd": 1000.0,
"price_rub": 100000.0,
"price_source": "custom",
"image_url": "",
"crates_names": ["create: dildo_case"],
"float_range": [0.00, 0.07],
"can_be_assigned_from": ["create: dildo_case"],
"is_craftable": True
},
{
"market_hash_name": "★ Dildo | Crimson Web (Minimal Wear)",
"weapon": "Dildo",
"pattern": "Crimson Web",
"rarity": "Covert",
"collection": "",
"type": "Normal",
"wear": "Minimal Wear",
"wear_range": [0.07, 0.15],
"mid_float_used": 0.10,
"doppler_phase": None,
"price_usd": 800.0,
"price_rub": 80000.0,
"price_source": "custom",
"image_url": "",
"crates_names": ["create: dildo_case"],
"float_range": [0.07, 0.15],
"can_be_assigned_from": ["create: dildo_case"],
"is_craftable": True
},
{
"market_hash_name": "Dildo | Blue Gem (Factory New)",
"weapon": "Dildo",
"pattern": "Blue Gem",
"rarity": "Classified",
"collection": "",
"type": "Normal",
"wear": "Factory New",
"wear_range": [0.00, 0.07],
"mid_float_used": 0.01,
"doppler_phase": None,
"price_usd": 500.0,
"price_rub": 50000.0,
"price_source": "custom",
"image_url": "",
"crates_names": ["create: dildo_case"],
"float_range": [0.00, 0.07],
"can_be_assigned_from": ["create: dildo_case"],
"is_craftable": True
}
]
try:
with CUSTOM_DATA_FILE.open("w", encoding="utf-8") as f:
json.dump(example_custom, f, ensure_ascii=False, indent=2)
print(f"Создан пример файла кастомных предметов: {CUSTOM_DATA_FILE}")
custom_items = example_custom
except Exception as e:
print(f"Не удалось создать файл кастомных предметов: {e}")
return custom_items
# ─── Генерация sub-вариаций цен и float ───────────────────────────────────
def generate_item_variants(num_variants: int = 6):
"""Для каждого предмета генерирует sub-вариации с разными ценами и float"""
global ALL_ITEMS
new_all = []
for item in ALL_ITEMS:
base_price = item.get("price_rub", 0)
wr = item.get("wear_range", [0, 1])
mid = item.get("mid_float_used", 0.5)
wr_min, wr_max = wr
f_range = max(wr_max - wr_min, 0.001)
for v in range(num_variants):
variant = dict(item)
t = v / (num_variants - 1) if num_variants > 1 else 0
# price factor: 0.55 to 1.40 across variants
factor = 0.55 + t * 0.85
# add small random jitter
factor += random.uniform(-0.05, 0.05)
factor = max(0.30, min(1.50, factor))
p = int(base_price * factor)
# scramble last digits
if p >= 1_000_000:
prefix = p // 1000
suffix = random.randint(100, 999)
p = prefix * 1000 + suffix
elif p >= 100_000:
prefix = p // 100
suffix = random.randint(10, 99)
p = prefix * 100 + suffix
elif p >= 10_000:
prefix = p // 10
suffix = random.randint(1, 9)
p = prefix * 10 + suffix
variant["price_rub"] = max(p, 100)
variant["price_usd"] = round(variant["price_rub"] / 100, 2)
# float spreads across the wear range
float_offset = -0.4 + t * 0.8
float_offset += random.uniform(-0.1, 0.1)
new_mid = mid + float_offset * f_range
new_mid = max(wr_min + 0.001, min(wr_max - 0.001, new_mid))
variant["mid_float_used"] = round(new_mid, 4)
new_all.append(variant)
count_before = len(ALL_ITEMS)
ALL_ITEMS = new_all
print(f"Сгенерировано {len(ALL_ITEMS)} sub-вариаций из {count_before} предметов (x{num_variants})")
# ─── Загрузка основных данных ─────────────────────────────────────────────
def load_base_items() -> List[dict]:
"""Загружает основные предметы из skins.json"""
try:
with DATA_FILE.open("r", encoding="utf-8") as f:
items = json.load(f)
print(f"Загружено {len(items):,} основных записей")
return items
except FileNotFoundError:
print(f"Критическая ошибка: файл {DATA_FILE} не найден")
sys.exit(1)
except Exception as e:
print(f"Ошибка чтения файла данных: {e}")
sys.exit(1)
# Загружаем все предметы (основные + кастомные)
BASE_ITEMS = load_base_items()
CUSTOM_ITEMS = load_custom_items()
# Объединяем предметы: сначала основные, потом кастомные
ALL_ITEMS = BASE_ITEMS + CUSTOM_ITEMS
print(f"Всего предметов после объединения: {len(ALL_ITEMS):,}")
# Генерируем sub-вариации цен и float для каждого предмета
generate_item_variants(num_variants=6)
print(f"Всего предметов после генерации вариаций: {len(ALL_ITEMS):,}")
ITEM_BY_ID: Dict[int, dict] = {i: item for i, item in enumerate(ALL_ITEMS)}
# ─── Глобальные кэши / индексы ──────────────────────────────────────────────
ALL_ITEMS_WITH_ID: List[dict] = []
ITEMS_BY_TYPE: Dict[str, List[dict]] = defaultdict(list)
ITEMS_BY_RARITY: Dict[str, List[dict]] = defaultdict(list)
NAME_LOWER_TO_IDS: Dict[str, List[int]] = defaultdict(list)
COLLECTIONS_INDEX: Dict[str, List[int]] = {}
CAN_BE_ASSIGNED_FROM_INDEX: Dict[str, List[int]] = defaultdict(list)
# ─── Проверка типа предмета ───────────────────────────────────────────────
def is_knife(item: dict) -> bool:
"""Проверяет, является ли предмет ножом"""
weapon = item.get("weapon", "")
return "Knife" in weapon or "Bayonet" in weapon
def is_gloves(item: dict) -> bool:
"""Проверяет, являются ли предмет перчатками"""
weapon = item.get("weapon", "")
return "Wraps" in weapon or "Gloves" in weapon or "Hand" in weapon
def is_rare_special_item(item: dict) -> bool:
"""Проверяет, является ли предмет Rare Special Item (ножи и перчатки)"""
return is_knife(item) or is_gloves(item)
# ─── Редкости ──────────────────────────────────────────────────────────────
RARITY_ORDER = {
"Consumer Grade": 0,
"Industrial Grade": 1,
"Mil-Spec": 2,
"Mil-Spec Grade": 2,
"Restricted": 3,
"Classified": 4,
"Covert": 5, # Базовый уровень для Covert
"Rare Special Item": 6, # Уровень для ножей/перчаток (конвертируется из Covert)
"Extraordinary": 7, # Для некоторых перчаток
"Contraband": 8,
"Unknown": -1,
}
def get_rarity_level(rarity: str | None, item: Optional[dict] = None) -> int:
"""
Возвращает числовой уровень редкости.
Для ножей и перчаток с редкостью Covert возвращает уровень Rare Special Item (6)
"""
if not rarity:
return -1
rarity = rarity.strip()
# Если это Extraordinary - это уже правильный уровень
if rarity == "Extraordinary":
return RARITY_ORDER["Extraordinary"]
# Если передан предмет и это Covert
if item and rarity == "Covert":
# Проверяем, является ли предмет ножом или перчатками
if is_rare_special_item(item):
return RARITY_ORDER["Rare Special Item"]
else:
return RARITY_ORDER["Covert"]
return RARITY_ORDER.get(rarity, -1)
# ─── Модели ────────────────────────────────────────────────────────────────
class ItemShort(BaseModel):
id: int
market_hash_name: str
rarity: Optional[str] = None
collection: Optional[str] = None
type: Optional[str] = None
price_rub: Optional[float] = None
price_usd: Optional[float] = None
class ContractSubmitRequest(BaseModel):
item_ids: List[int]
@field_validator("item_ids")
@classmethod
def check_length(cls, v):
if len(v) != 10:
raise ValueError("Нужно ровно 10 предметов")
return v
class TradeUpSimulationResult(BaseModel):
success: bool
input_items: List[ItemShort]
received_item: Optional[ItemShort] = None
received_float: Optional[float] = None
probability: Optional[float] = None
message: str
class CollectionItemBase(BaseModel):
base_name: str
rarity: str
item_ids_normal: List[int]
item_ids_stattrak: List[int]
item_ids_souvenir: List[int]
count_variants: int
class CollectionDetail(BaseModel):
collection: str
total_unique_skins: int
items: List[CollectionItemBase]
# ─── Вспомогательные ───────────────────────────────────────────────────────
def get_item(idx: int) -> Optional[dict]:
return ITEM_BY_ID.get(idx)
def is_final_in_collection(item: dict) -> bool:
"""Проверяет, есть ли предметы выше редкостью в той же коллекции/источнике"""
my_level = get_rarity_level(item.get("rarity"), item)
# Собираем все источники, откуда мог выпасть предмет
sources = []
# Из collection
coll = item.get("collection", "").strip()
if coll and coll != "Без коллекции":
sources.append(coll)
# Из can_be_assigned_from
assigned = item.get("can_be_assigned_from", [])
if isinstance(assigned, list):
sources.extend(assigned)
elif isinstance(assigned, str) and assigned:
sources.append(assigned)
# Из crates_names (кейсы)
crates = item.get("crates_names", [])
if crates and isinstance(crates, list):
for crate in crates:
sources.append(f"Case: {crate}")
# Проверяем каждый источник
for source in sources:
# Проверяем в COLLECTIONS_INDEX
for idx in COLLECTIONS_INDEX.get(source, []):
other = get_item(idx)
if other:
other_level = get_rarity_level(other.get("rarity"), other)
if other_level > my_level:
return False
# Проверяем в CAN_BE_ASSIGNED_FROM_INDEX
for idx in CAN_BE_ASSIGNED_FROM_INDEX.get(source, []):
other = get_item(idx)
if other:
other_level = get_rarity_level(other.get("rarity"), other)
if other_level > my_level:
return False
return True
def can_be_upgraded(items: List[dict]) -> tuple[bool, str]:
"""Проверяет, можно ли использовать эти предметы в контракте"""
if len(items) != 10:
return False, "Нужно ровно 10 предметов"
# Проверяем is_craftable
for item in items:
if not item.get("is_craftable", True):
return False, f"Предмет нельзя использовать в крафте: {item.get('market_hash_name', '???')}"
# Проверяем, что ни один предмет не является финальным
for item in items:
if is_final_in_collection(item):
return False, f"Нельзя использовать финальный предмет коллекции: {item.get('market_hash_name', '???')}"
# Проверяем тип (Normal/StatTrak/Souvenir)
types = {it.get("type") for it in items if it.get("type")}
if len(types) != 1:
return False, "Все предметы должны быть одного типа"
# Проверяем редкость с учетом типа предмета
rarities = set()
for it in items:
level = get_rarity_level(it.get("rarity"), it)
rarities.add(level)
if len(rarities) != 1:
return False, "Все предметы должны быть одной редкости"
return True, ""
def extract_base_name(market_hash_name: str) -> str:
"""Извлекает базовое имя скина без StatTrak, Souvenir и качества"""
name = market_hash_name.strip()
# Убираем префиксы
for prefix in ["StatTrak™ ", "Souvenir "]:
if name.startswith(prefix):
name = name[len(prefix):].strip()
# Убираем качество в скобках
if " (" in name:
name = name.split(" (", 1)[0].strip()
return name
def build_indexes():
"""Строит все индексы"""
global COLLECTIONS_INDEX, CAN_BE_ASSIGNED_FROM_INDEX
coll = defaultdict(list)
assigned_from = defaultdict(list)
for idx, item in ITEM_BY_ID.items():
# Индекс коллекций
c = item.get("collection", "").strip()
if c and c != "Без коллекции":
coll[c].append(idx)
# Индекс can_be_assigned_from
assigned = item.get("can_be_assigned_from", [])
if isinstance(assigned, list):
for source in assigned:
assigned_from[source].append(idx)
elif isinstance(assigned, str) and assigned:
assigned_from[assigned].append(idx)
# Индекс кейсов (crates_names)
crates = item.get("crates_names", [])
if crates and isinstance(crates, list):
for crate in crates:
collection_name = f"Case: {crate}"
coll[collection_name].append(idx)
COLLECTIONS_INDEX = dict(coll)
CAN_BE_ASSIGNED_FROM_INDEX = dict(assigned_from)
def load_and_index_data():
"""Индексирует все данные для быстрого поиска"""
global ALL_ITEMS_WITH_ID, ITEMS_BY_TYPE, ITEMS_BY_RARITY, NAME_LOWER_TO_IDS
ALL_ITEMS_WITH_ID = []
ITEMS_BY_TYPE.clear()
ITEMS_BY_RARITY.clear()
NAME_LOWER_TO_IDS.clear()
print("Создание индексов...")
for i in tqdm(range(len(ALL_ITEMS)), desc="Индексация"):
item = ALL_ITEMS[i]
item_with_id = {**item, "id": i}
ALL_ITEMS_WITH_ID.append(item_with_id)
ITEM_BY_ID[i] = item
# По типу
t = item.get("type", "Normal")
ITEMS_BY_TYPE[t].append(item_with_id)
# По редкости
r = item.get("rarity", "Unknown")
ITEMS_BY_RARITY[r].append(item_with_id)
# Поиск по имени
name = item.get("market_hash_name", "").lower().strip()
if name:
NAME_LOWER_TO_IDS[name].append(i)
base_name = extract_base_name(item.get("market_hash_name", "")).lower().strip()
if base_name and base_name != name:
NAME_LOWER_TO_IDS[base_name].append(i)
# Строим индексы коллекций
build_indexes()
print(f"Готово. Уникальных имён для поиска: {len(NAME_LOWER_TO_IDS):,}")
print(f"Типов: {len(ITEMS_BY_TYPE)}")
print(f"Редкостей: {len(ITEMS_BY_RARITY)}")
print(f"Коллекций/кейсов: {len(COLLECTIONS_INDEX)}")
print(f"Источников (can_be_assigned_from): {len(CAN_BE_ASSIGNED_FROM_INDEX)}")
def warmup_and_debug_data():
"""Диагностика данных при старте"""
print("\n" + "="*80)
print("ПРОГРЕВ ДАННЫХ И ДИАГНОСТИКА")
print("="*80)
rarities = Counter()
types_cnt = Counter()
knife_count = 0
glove_count = 0
for item in ALL_ITEMS:
rarities[item.get("rarity", "Без редкости")] += 1
types_cnt[item.get("type", "Без типа")] += 1
if is_knife(item):
knife_count += 1
elif is_gloves(item):
glove_count += 1
print(f"Ножей: {knife_count:,}")
print(f"Перчаток: {glove_count:,}")
print(f"Rare Special Items всего: {knife_count + glove_count:,}")
print("\nРедкости:")
for r, c in sorted(rarities.items(), key=lambda x: x[1], reverse=True):
print(f" {r:24}{c:6,}")
print("\nТипы:")
for t, c in types_cnt.most_common(5):
print(f" {t:24}{c:6,}")
# Проверяем Covert предметы
covert_items = [it for it in ALL_ITEMS if it.get("rarity") == "Covert"]
covert_knives = [it for it in covert_items if is_knife(it)]
covert_skins = [it for it in covert_items if not is_knife(it) and not is_gloves(it)]
print(f"\nCovert предметы:")
print(f" Всего: {len(covert_items):,}")
print(f" Ножи: {len(covert_knives):,}")
print(f" Обычные скины: {len(covert_skins):,}")
# Проверяем кейсы
print(f"\nКейсов (Case: *): {sum(1 for k in COLLECTIONS_INDEX.keys() if k.startswith('Case:')):,}")
print("="*80 + "\n")
# Загружаем и индексируем данные
load_and_index_data()
warmup_and_debug_data()
def calculate_average_float(items: List[dict]) -> float:
"""Вычисляет средний float из входных предметов"""
if not items:
return 0.5
return sum(it.get("mid_float_used", 0.5) for it in items) / len(items)
def get_possible_outcomes(items: List[dict]) -> tuple[List[tuple], Dict[str, float]]:
"""
Определяет возможные исходы контракта и их вероятности.
Возвращает список (item_id, probability) и словарь весов источников.
"""
input_type = items[0].get("type", "Normal")
first_item = items[0]
# Определяем текущий уровень редкости
current_level = get_rarity_level(first_item.get("rarity"), first_item)
target_level = current_level + 1
# Собираем источники из входных предметов
source_counter = Counter()
for it in items:
# Из can_be_assigned_from
assigned = it.get("can_be_assigned_from", [])
if isinstance(assigned, list):
for source in assigned:
source_counter[source] += 1
elif isinstance(assigned, str) and assigned:
source_counter[assigned] += 1
# Из collection
coll = it.get("collection", "").strip()
if coll and coll != "Без коллекции":
source_counter[coll] += 1
# Из crates_names
crates = it.get("crates_names", [])
if crates and isinstance(crates, list):
for crate in crates:
source_counter[f"Case: {crate}"] += 1
total = sum(source_counter.values())
if total == 0:
return [], {}
# Веса источников пропорциональны количеству предметов из них
source_weights = {s: cnt / total for s, cnt in source_counter.items()}
# Группируем возможные исходы по базовому имени
base_to_ids = defaultdict(list)
for source_name in source_weights.keys():
# Проверяем в COLLECTIONS_INDEX
for idx in COLLECTIONS_INDEX.get(source_name, []):
cand = get_item(idx)
if not cand:
continue
# Проверяем уровень редкости
cand_level = get_rarity_level(cand.get("rarity"), cand)
if cand_level != target_level:
continue
# Проверяем тип
if cand.get("type") != input_type:
continue
base = extract_base_name(cand["market_hash_name"])
base_to_ids[base].append(idx)
# Проверяем в CAN_BE_ASSIGNED_FROM_INDEX
for idx in CAN_BE_ASSIGNED_FROM_INDEX.get(source_name, []):
cand = get_item(idx)
if not cand:
continue
cand_level = get_rarity_level(cand.get("rarity"), cand)
if cand_level != target_level:
continue
if cand.get("type") != input_type:
continue
base = extract_base_name(cand["market_hash_name"])
base_to_ids[base].append(idx)
if not base_to_ids:
return [], source_weights
# Равномерный шанс на каждый уникальный базовый скин
num_unique = len(base_to_ids)
prob_per_base = 1.0 / num_unique
possible_outcomes = []
for base, ids_list in base_to_ids.items():
prob_per_id = prob_per_base / len(ids_list)
for idx in ids_list:
possible_outcomes.append((idx, prob_per_id))
return possible_outcomes, source_weights
def simulate_trade_up(items: List[dict]) -> TradeUpSimulationResult:
"""Симулирует контракт обмена 10→1"""
# Подготовка input_items
input_short = []
for it in items:
if isinstance(it, dict):
enriched = it.copy()
else:
enriched = {}
if "id" not in enriched:
enriched["id"] = -1
try:
short = ItemShort(**enriched)
input_short.append(short)
except Exception:
input_short.append(ItemShort(
id=enriched.get("id", -1),
market_hash_name=enriched.get("market_hash_name", "???")
))
ok, msg = can_be_upgraded(items)
if not ok:
return TradeUpSimulationResult(
success=False,
input_items=input_short,
received_item=None,
received_float=None,
probability=None,
message=msg
)
possible_outcomes, source_weights = get_possible_outcomes(items)
if not possible_outcomes:
input_type = items[0].get("type", "Normal")
first_item = items[0]
current_level = get_rarity_level(first_item.get("rarity"), first_item)
target_level = current_level + 1
# Определяем название целевой редкости
rarity_names = {v: k for k, v in RARITY_ORDER.items()}
target_rarity_name = rarity_names.get(target_level, f"уровень {target_level}")
return TradeUpSimulationResult(
success=False,
input_items=input_short,
received_item=None,
received_float=None,
probability=None,
message=f"Нет {input_type} предметов редкостью {target_rarity_name} в выбранных источниках"
)
# Выбираем случайный предмет
indices = [o[0] for o in possible_outcomes]
weights = [o[1] for o in possible_outcomes]
chosen_idx = random.choices(indices, weights=weights, k=1)[0]
received = get_item(chosen_idx)
# Вычисляем float (среднее арифметическое входных float)
avg_float = calculate_average_float(items)
# Находим вероятность выпавшего предмета
probability = next((p for idx, p in possible_outcomes if idx == chosen_idx), 0)
return TradeUpSimulationResult(
success=True,
input_items=input_short,
received_item=ItemShort(**{**received, "id": chosen_idx}),
received_float=round(avg_float, 4),
probability=round(probability * 100, 2),
message="Контракт успешен"
)
# ─── Эндпоинты ─────────────────────────────────────────────────────────────
@app.get("/items")
def get_all_items(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
search: Optional[str] = Query(None),
id: Optional[int] = Query(None),
item_type: Optional[str] = Query(None),
rarity: Optional[str] = Query(None),
min_price: Optional[float] = Query(None, ge=0),
max_price: Optional[float] = Query(None, ge=0),
wear: Optional[str] = Query(None),
):
"""Возвращает список предметов с фильтрацией и поиском"""
if id is not None:
item = get_item(id)
if item is None:
return []
return [ItemShort(**{**item, "id": id})]
# Базовый набор кандидатов
if item_type and item_type.lower() != "all":
candidates = ITEMS_BY_TYPE.get(item_type, [])
else:
candidates = ALL_ITEMS_WITH_ID
# Фильтр по редкости
if rarity:
r_norm = rarity.strip()
candidates = [it for it in candidates if it.get("rarity", "").strip() == r_norm]
# Поиск по имени
if search:
query = search.lower().strip()
found_ids = set()
# Точное совпадение
if query in NAME_LOWER_TO_IDS:
found_ids.update(NAME_LOWER_TO_IDS[query])
# Частичное совпадение по словам
search_words = query.split()
for name_lower, ids in NAME_LOWER_TO_IDS.items():
if all(word in name_lower for word in search_words):
found_ids.update(ids)
elif name_lower.startswith(query):
found_ids.update(ids)
if not found_ids:
return []
candidates = [it for it in candidates if it["id"] in found_ids]
# Фильтры по цене и wear
filtered = candidates
if min_price is not None or max_price is not None:
filtered = [
it for it in filtered
if it.get("price_rub") is not None and
(min_price is None or it["price_rub"] >= min_price) and
(max_price is None or it["price_rub"] <= max_price)
]
if wear:
wear_norm = wear.strip()
filtered = [it for it in filtered if it.get("wear", "").strip() == wear_norm]
# Пагинация
sliced = filtered[skip:skip + limit]
return [ItemShort(**it) for it in sliced]
@app.get("/filters")
def get_filters():
"""Возвращает доступные значения для фильтров"""
rarities = set()
types = set()
wears = set()
price_range = {"min": float('inf'), "max": float('-inf')}
knife_count = 0
glove_count = 0
normal_count = 0
for item in ALL_ITEMS:
if item.get("rarity"):
rarities.add(item["rarity"])
if item.get("type"):
types.add(item["type"])
if item.get("wear"):
wears.add(item["wear"])
if item.get("price_rub"):
price = item["price_rub"]
if price < price_range["min"]:
price_range["min"] = price
if price > price_range["max"]:
price_range["max"] = price
if is_knife(item):
knife_count += 1
elif is_gloves(item):
glove_count += 1
else:
normal_count += 1
return {
"rarities": sorted(list(rarities)),
"types": sorted(list(types)),
"wears": sorted(list(wears)),
"price_range": {
"min_rub": price_range["min"] if price_range["min"] != float('inf') else 0,
"max_rub": price_range["max"] if price_range["max"] != float('-inf') else 0
},
"statistics": {
"total_items": len(ALL_ITEMS),
"knives": knife_count,
"gloves": glove_count,
"normal_skins": normal_count,
"note": "Ножи и перчатки нельзя использовать в trade-up контрактах"
}
}
@app.post("/contract/submit", response_model=TradeUpSimulationResult)
def submit_contract(req: ContractSubmitRequest):
"""Выполняет симуляцию контракта обмена"""
try:
selected = []
for idx in req.item_ids:
item = get_item(idx)
if item is None:
raise HTTPException(400, detail=f"id {idx} не найден")
item_with_id = item.copy()
item_with_id["id"] = idx
selected.append(item_with_id)
return simulate_trade_up(selected)
except Exception as e:
import traceback
traceback.print_exc()
raise HTTPException(500, detail=str(e))
@app.get("/collections/")
def get_collections_list():
"""Возвращает список всех коллекций и источников"""
result = []
all_sources = set(COLLECTIONS_INDEX.keys()) | set(CAN_BE_ASSIGNED_FROM_INDEX.keys())
for name in sorted(all_sources):
ids = set()
if name in COLLECTIONS_INDEX:
ids.update(COLLECTIONS_INDEX[name])
if name in CAN_BE_ASSIGNED_FROM_INDEX:
ids.update(CAN_BE_ASSIGNED_FROM_INDEX[name])
unique_bases = set()
for idx in ids:
item = get_item(idx)
if item:
base = extract_base_name(item.get("market_hash_name", ""))
if base:
unique_bases.add(base)
result.append({
"name": name,
"total_items": len(ids),
"unique_skins": len(unique_bases),
"is_case": name.startswith("Case: ")
})
return {
"total_collections": len(result),
"collections": result
}
@app.get("/collections/all")
def get_collection_names():
"""Возвращает подробную информацию о всех коллекциях"""
def rarity_key(idx: int) -> int:
item = get_item(idx)
return get_rarity_level(item.get("rarity"), item) if item else -1
result = []
all_sources = set(COLLECTIONS_INDEX.keys()) | set(CAN_BE_ASSIGNED_FROM_INDEX.keys())
for source_name in sorted(all_sources):
ids = set()
if source_name in COLLECTIONS_INDEX:
ids.update(COLLECTIONS_INDEX[source_name])
if source_name in CAN_BE_ASSIGNED_FROM_INDEX:
ids.update(CAN_BE_ASSIGNED_FROM_INDEX[source_name])
base_names = set()
items_info = []
for i in ids:
item = get_item(i)
if item:
base = extract_base_name(item["market_hash_name"])
if base:
base_names.add(base)
items_info.append({
"id": i,
"name": item.get("market_hash_name", "Unknown"),
"rarity": item.get("rarity", "Unknown"),
"type": item.get("type", "Normal"),
"wear": item.get("wear", "Unknown"),
"price_rub": item.get("price_rub"),
"price_usd": item.get("price_usd"),
"is_craftable": item.get("is_craftable", True)
})
sorted_ids = sorted(ids, key=rarity_key)
items_info.sort(key=lambda x: (
get_rarity_level(x["rarity"], get_item(x["id"])),
x["name"]
))
result.append({
"name": source_name,
"unique_skin_count": len(base_names),
"total_variants": len(ids),
"item_ids_by_rarity": sorted_ids,
"items": items_info
})
return result
@app.get("/collection/{name}", response_model=CollectionDetail)
def get_one_collection(name: str):
"""Возвращает детальную информацию о конкретной коллекции/источнике"""
ids = set()
if name in COLLECTIONS_INDEX:
ids.update(COLLECTIONS_INDEX[name])
if name in CAN_BE_ASSIGNED_FROM_INDEX:
ids.update(CAN_BE_ASSIGNED_FROM_INDEX[name])
if not ids:
raise HTTPException(404, detail="Коллекция или источник не найдены")
groups = defaultdict(list)
for idx in ids:
item = get_item(idx)
if not item:
continue
base = extract_base_name(item["market_hash_name"])
if not base:
continue
groups[base].append((
item.get("mid_float_used", 999.9),
idx,
item.get("type", "Normal"),
item.get("rarity", "Unknown")
))
result_items = []
for base_name, variants in sorted(groups.items()):
rarity = next((v[3] for v in variants if v[3] != "Unknown"), "Unknown")
normal = [v[1] for v in sorted(variants) if v[2] == "Normal"]
stattrak = [v[1] for v in sorted(variants) if v[2] == "StatTrak"]
souvenir = [v[1] for v in sorted(variants) if v[2] == "Souvenir"]
result_items.append(CollectionItemBase(
base_name=base_name,
rarity=rarity,
item_ids_normal=normal,
item_ids_stattrak=stattrak,
item_ids_souvenir=souvenir,
count_variants=len(variants)
))
return CollectionDetail(
collection=name,
total_unique_skins=len(result_items),
items=result_items
)
@app.get("/health")
def health():
"""Проверка работоспособности сервера"""
return {
"status": "ok",
"items_loaded": len(ALL_ITEMS),
"base_items": len(BASE_ITEMS),
"custom_items": len(CUSTOM_ITEMS)
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=13668)