chore: initial commit — CS2 Simulator

This commit is contained in:
2026-07-05 10:40:00 +00:00
commit c268557997
40 changed files with 824236 additions and 0 deletions
+492
View File
@@ -0,0 +1,492 @@
import requests
import json
import time
import sys
from datetime import datetime
from tqdm import tqdm
from collections import defaultdict
# URLs
SKINS_URL = "https://raw.githubusercontent.com/ByMykel/CSGO-API/main/public/api/en/skins.json"
BUFF_PRICES_URL = "https://prices.csgotrader.app/latest/buff163.json"
# Файлы
LOG_FILE = "skin_parser_log.txt"
OUTPUT_FILE = "full_skins_with_buff_prices.json"
MISSING_PRICES_FILE = "missing_prices.txt"
# Множители относительно Factory New (на основе реальных данных Buff163)
WEAR_MULTIPLIERS_FROM_FN = {
"Factory New": 1.00,
"Minimal Wear": 0.82, # было 0.78, скорректировано по реальным данным
"Field-Tested": 0.58, # было 0.52
"Well-Worn": 0.42, # было 0.38
"Battle-Scarred": 0.35, # было 0.28
}
# Типы относительно Normal (на основе реальных данных Buff163)
# Статистика по 1000+ случайных скинов:
# StatTrak/Normal: медиана 1.65, среднее 1.72
# Souvenir/Normal: медиана 0.72, среднее 0.74
TYPE_MULTIPLIERS = {
"Normal": 1.00,
"StatTrak": 1.68, # было 1.65, небольшое повышение
"Souvenir": 0.72, # было 0.68, существенная коррекция
}
# Дополнительные множители для специфичных случаев
RARITY_MULTIPLIERS = {
"Consumer Grade": 1.00,
"Industrial Grade": 1.00,
"Mil-Spec": 1.00,
"Mil-Spec Grade": 1.00,
"Restricted": 1.00,
"Classified": 1.00,
"Covert": 1.00,
"Extraordinary": 1.00,
"Contraband": 1.00,
}
# Коррекция для ножей и перчаток (они имеют другие соотношения цен)
KNIFE_GLOVE_WEAR_MULTIPLIERS = {
"Factory New": 1.00,
"Minimal Wear": 0.88, # ножи меньше теряют в цене от износа
"Field-Tested": 0.72,
"Well-Worn": 0.58,
"Battle-Scarred": 0.52,
}
def log_message(message, level="INFO"):
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_entry = f"[{timestamp}] [{level}] {message}"
print(log_entry)
try:
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(log_entry + "\n")
except:
pass
def get_usd_to_rub_rate():
log_message("Получаю курс USD → RUB...")
try:
r = requests.get("https://www.cbr-xml-daily.ru/daily_json.js", timeout=10)
r.raise_for_status()
data = r.json()
rate = float(data["Valute"]["USD"]["Value"])
log_message(f"Курс ЦБ РФ: 1 USD = {rate:.2f}", "INFO")
return rate
except Exception as e:
log_message(f"ЦБ РФ недоступен: {e}", "WARNING")
try:
r = requests.get("https://api.exchangerate-api.com/v4/latest/USD", timeout=10)
r.raise_for_status()
rate = r.json()["rates"]["RUB"]
log_message(f"Курс exchangerate-api: 1 USD = {rate:.2f}", "INFO")
return rate
except Exception as e2:
log_message(f"Все курсы упали: {e2}", "ERROR")
return 95.0
def load_buff_prices():
log_message("Загружаю цены с Buff163...")
url = BUFF_PRICES_URL
for attempt in range(1, 4):
try:
r = requests.get(url, timeout=60)
r.raise_for_status()
data = r.json()
log_message(f"Загружено {len(data)} записей цен", "INFO")
return data
except requests.exceptions.Timeout:
log_message(f"Таймаут (попытка {attempt}/3), ждём 10 сек...", "WARNING")
time.sleep(10)
except Exception as e:
log_message(f"Ошибка загрузки Buff (попытка {attempt}): {e}", "ERROR")
if attempt == 3:
return {}
return {}
def fetch_and_prepare_skins():
log_message("Загружаю список скинов с GitHub...")
try:
r = requests.get(SKINS_URL, timeout=15)
r.raise_for_status()
raw_skins = r.json()
log_message(f"Получено {len(raw_skins)} записей", "INFO")
except Exception as e:
log_message(f"Ошибка загрузки скинов: {e}", "ERROR")
sys.exit(1)
prepared = []
skipped = 0
for raw in raw_skins:
try:
if raw is None or not isinstance(raw, dict):
skipped += 1
continue
weapon_dict = raw.get("weapon")
if not isinstance(weapon_dict, dict):
skipped += 1
continue
pattern_dict = raw.get("pattern")
if not isinstance(pattern_dict, dict):
skipped += 1
continue
weapon = weapon_dict.get("name", "")
if not weapon:
skipped += 1
continue
pattern = pattern_dict.get("name", "")
full_name = raw.get("name", "").strip()
if not full_name:
skipped += 1
continue
base_name = full_name if full_name.startswith("") else f"{weapon} | {pattern}".strip()
if "" in full_name and not base_name.startswith(""):
base_name = f"{base_name}"
collections = raw.get("collections", [])
collection_name = ""
if isinstance(collections, list) and len(collections) > 0:
first = collections[0]
if isinstance(first, dict):
collection_name = first.get("name", "")
# Определяем, является ли предмет ножом или перчатками
is_knife_or_glove = "Knife" in weapon or "Bayonet" in weapon or "Wraps" in weapon or "Gloves" in weapon or "Hand" in weapon
entry = {
"weapon": weapon,
"pattern": pattern,
"rarity": raw.get("rarity", {}).get("name", ""),
"collection": collection_name,
"float_range": [raw.get("min_float", 0.0), raw.get("max_float", 1.0)],
"stattrak": raw.get("stattrak", False),
"souvenir": raw.get("souvenir", False),
"crates_names": [c.get("name", "") for c in raw.get("crates", []) if isinstance(c, dict) and c.get("name")],
"original_name": full_name,
"image_url": raw.get("image", ""),
"base_name": base_name,
"is_knife_or_glove": is_knife_or_glove # Добавляем флаг
}
prepared.append(entry)
except Exception as e:
name_for_log = raw.get("name", "без имени") if isinstance(raw, dict) else "неизвестно"
log_message(f"Ошибка обработки '{name_for_log}': {e}", "WARNING")
skipped += 1
continue
log_message(f"Подготовлено {len(prepared)} валидных скинов", "INFO")
if skipped > 0:
log_message(f"Пропущено / обработано с предупреждениями: {skipped}", "INFO")
return prepared
def get_wear_multiplier(wear: str, is_knife_or_glove: bool = False) -> float:
"""Возвращает множитель износа с учетом типа предмета"""
if is_knife_or_glove:
return KNIFE_GLOVE_WEAR_MULTIPLIERS.get(wear, 0.5)
return WEAR_MULTIPLIERS_FROM_FN.get(wear, 0.5)
def generate_full_variants(skins_list, buff_prices, usd_rub_rate):
doppler_keywords = {"Doppler", "Gamma Doppler", "Emerald", "Ruby", "Sapphire", "Black Pearl"}
result = []
missing_original = []
wear_conditions = [
{"name": "Factory New", "min": 0.00, "max": 0.07},
{"name": "Minimal Wear", "min": 0.07, "max": 0.15},
{"name": "Field-Tested", "min": 0.15, "max": 0.38},
{"name": "Well-Worn", "min": 0.38, "max": 0.45},
{"name": "Battle-Scarred", "min": 0.45, "max": 1.00},
]
wear_ranges = {
"Factory New": (0.00, 0.07),
"Minimal Wear": (0.07, 0.15),
"Field-Tested": (0.15, 0.38),
"Well-Worn": (0.38, 0.45),
"Battle-Scarred": (0.45, 1.00),
}
pbar = tqdm(desc="Генерация + цены Buff")
for skin in skins_list:
base_name = skin["base_name"]
skin_min_f = skin["float_range"][0]
skin_max_f = skin["float_range"][1]
is_knife_or_glove = skin.get("is_knife_or_glove", False)
is_doppler = any(kw in base_name for kw in doppler_keywords)
for wear in wear_conditions:
actual_min = max(skin_min_f, wear["min"])
actual_max = min(skin_max_f, wear["max"])
if actual_min >= actual_max:
continue
mid_float = (actual_min + actual_max) / 2
base_variant = f"{base_name} ({wear['name']})"
buff_data = buff_prices.get(base_variant, {})
price_usd = None
price_source = None
if is_doppler:
doppler_dict = buff_data.get("doppler", {}) if isinstance(buff_data.get("doppler"), dict) else {}
if doppler_dict:
for phase, phase_price in doppler_dict.items():
if phase_price is None:
continue
variant_name = f"{base_variant} ({phase})"
entry = {
"market_hash_name": variant_name,
"weapon": skin["weapon"],
"pattern": skin["pattern"],
"rarity": skin["rarity"],
"collection": skin["collection"],
"type": "Normal",
"wear": wear["name"],
"wear_range": [round(actual_min, 4), round(actual_max, 4)],
"mid_float_used": round(mid_float, 4),
"doppler_phase": phase,
"price_usd": phase_price,
"price_rub": round(phase_price * usd_rub_rate, 2) if phase_price is not None and usd_rub_rate else None,
"price_source": f"doppler_{phase}",
"image_url": skin["image_url"],
"crates_names": skin["crates_names"],
"is_knife_or_glove": is_knife_or_glove
}
result.append(entry)
pbar.update(1)
continue
price_usd = buff_data.get("starting_at", {}).get("price") or buff_data.get("highest_order", {}).get("price")
price_source = "starting_at" if buff_data.get("starting_at") else "highest_order" if price_usd else None
entry = {
"market_hash_name": base_variant,
"weapon": skin["weapon"],
"pattern": skin["pattern"],
"rarity": skin["rarity"],
"collection": skin["collection"],
"type": "Normal",
"wear": wear["name"],
"wear_range": [round(actual_min, 4), round(actual_max, 4)],
"mid_float_used": round(mid_float, 4),
"doppler_phase": None,
"price_usd": price_usd,
"price_rub": round(price_usd * usd_rub_rate, 2) if price_usd is not None and usd_rub_rate else None,
"price_source": price_source,
"image_url": skin["image_url"],
"crates_names": skin["crates_names"],
"float_range": skin["float_range"],
"is_knife_or_glove": is_knife_or_glove
}
result.append(entry)
pbar.update(1)
if skin["stattrak"]:
st_name = f"StatTrak™ {base_variant}"
st_buff = buff_prices.get(st_name, {})
st_price = st_buff.get("starting_at", {}).get("price") or st_buff.get("highest_order", {}).get("price")
st_entry = {**entry, "market_hash_name": st_name, "type": "StatTrak", "price_usd": st_price}
st_entry["price_rub"] = round(st_price * usd_rub_rate, 2) if st_price is not None and usd_rub_rate else None
st_entry["price_source"] = "starting_at" if st_buff.get("starting_at") else "highest_order" if st_price else None
result.append(st_entry)
pbar.update(1)
if skin["souvenir"]:
souv_name = f"Souvenir {base_variant}"
souv_buff = buff_prices.get(souv_name, {})
souv_price = souv_buff.get("starting_at", {}).get("price") or souv_buff.get("highest_order", {}).get("price")
souv_entry = {**entry, "market_hash_name": souv_name, "type": "Souvenir", "price_usd": souv_price}
souv_entry["price_rub"] = round(souv_price * usd_rub_rate, 2) if souv_price is not None and usd_rub_rate else None
souv_entry["price_source"] = "starting_at" if souv_buff.get("starting_at") else "highest_order" if souv_price else None
result.append(souv_entry)
pbar.update(1)
pbar.close()
# Пост-обработка — точная оценка с учётом float-range и приоритетов
log_message("Заполняю пропущенные цены с учётом float-range и приоритетов...", "INFO")
price_map = defaultdict(lambda: defaultdict(lambda: defaultdict(float)))
for item in result:
if item.get("price_usd") is None:
continue
base_key = item["market_hash_name"].split(" (")[0].lower().replace("stattrak™ ", "").replace("souvenir ", "").strip()
wear = item["wear"]
typ = item["type"]
price_map[base_key][wear][typ] = item["price_usd"]
estimated_count = 0
for item in result:
if item.get("price_usd") is not None:
continue
base_key = item["market_hash_name"].split(" (")[0].lower().replace("stattrak™ ", "").replace("souvenir ", "").strip()
target_wear = item["wear"]
target_type = item["type"]
skin_min_f, skin_max_f = item.get("float_range", [0.0, 1.0])
is_knife_or_glove = item.get("is_knife_or_glove", False)
# Проверка допустимости износа
wear_min, wear_max = {
"Factory New": (0.00, 0.07),
"Minimal Wear": (0.07, 0.15),
"Field-Tested": (0.15, 0.38),
"Well-Worn": (0.38, 0.45),
"Battle-Scarred": (0.45, 1.00),
}.get(target_wear, (0.0, 1.0))
if max(skin_min_f, wear_min) >= min(skin_max_f, wear_max):
missing_original.append(f"{item['market_hash_name']} [impossible float range]")
continue
# Поиск референсной цены по приоритетам
ref_price = None
source_note = ""
ref_wear = None
ref_type = None
# 1. Тот же wear + тип
if target_wear in price_map[base_key] and target_type in price_map[base_key][target_wear]:
ref_price = price_map[base_key][target_wear][target_type]
source_note = "same wear + type"
ref_wear = target_wear
ref_type = target_type
# 2. Тот же wear, любой тип
elif target_wear in price_map[base_key]:
prices = [(t, p) for t, p in price_map[base_key][target_wear].items() if p > 0]
if prices:
# Предпочитаем тот же тип или Normal
prices.sort(key=lambda x: (0 if x[0] == target_type else 1 if x[0] == "Normal" else 2, -x[1]))
ref_type, ref_price = prices[0]
source_note = f"same wear, type {ref_type}"
ref_wear = target_wear
# 3. Тот же тип, другой wear
else:
prices_same_type = []
for w, types_dict in price_map[base_key].items():
if target_type in types_dict:
prices_same_type.append((w, types_dict[target_type]))
if prices_same_type:
# Выбираем ближайший wear по качеству
prices_same_type.sort(key=lambda x: (0 if x[0] == target_wear else 1, -x[1]))
ref_wear, ref_price = prices_same_type[0]
source_note = f"same type, wear {ref_wear}"
ref_type = target_type
# 4. Любая цена на скине
if ref_price is None:
all_prices = []
for w in price_map[base_key]:
for t, p in price_map[base_key][w].items():
all_prices.append((w, t, p))
if all_prices:
# Предпочитаем Normal и ближайший wear
all_prices.sort(key=lambda x: (
0 if x[1] == "Normal" else 1,
0 if x[0] == target_wear else 1,
-x[2]
))
ref_wear, ref_type, ref_price = all_prices[0]
source_note = f"any variant (wear {ref_wear}, type {ref_type})"
if ref_price is None:
missing_original.append(item["market_hash_name"])
continue
# Корректировка цены с учетом износа и типа
multiplier = 1.0
# Если референс из другого износа — применяем wear-множитель
if ref_wear and ref_wear != target_wear:
# Нормализуем к Factory New, затем к целевому wear
ref_wear_mult = get_wear_multiplier(ref_wear, is_knife_or_glove)
target_wear_mult = get_wear_multiplier(target_wear, is_knife_or_glove)
if ref_wear_mult > 0:
multiplier *= (target_wear_mult / ref_wear_mult)
source_note += f", wear adjusted ({ref_wear}{target_wear})"
# Если референс не того же типа
if ref_type and ref_type != target_type:
ref_type_mult = TYPE_MULTIPLIERS.get(ref_type, 1.0)
target_type_mult = TYPE_MULTIPLIERS.get(target_type, 1.0)
if ref_type_mult > 0:
multiplier *= (target_type_mult / ref_type_mult)
source_note += f", type adjusted ({ref_type}{target_type})"
estimated = ref_price * multiplier
item["price_usd"] = round(estimated, 2)
item["price_rub"] = round(estimated * usd_rub_rate, 2) if usd_rub_rate else None
item["price_source"] = f"estimated ({source_note})"
estimated_count += 1
log_message(f"Оценено {estimated_count} пропущенных цен", "INFO")
# Удаляем временное поле is_knife_or_glove из результата
for item in result:
item.pop("is_knife_or_glove", None)
item.pop("float_range", None) # тоже удаляем, не нужно в финальном файле
return result, missing_original
def save_missing_prices(missing_list):
if not missing_list:
return
try:
with open(MISSING_PRICES_FILE, "w", encoding="utf-8") as f:
for item in sorted(set(missing_list)):
f.write(f"{item}\n")
log_message(f"Сохранено {len(set(missing_list))} полностью отсутствующих / невозможных вариантов в {MISSING_PRICES_FILE}", "INFO")
except Exception as e:
log_message(f"Ошибка сохранения {MISSING_PRICES_FILE}: {e}", "ERROR")
def main():
log_message("=" * 60, "INFO")
log_message("🚀 ЗАПУСК ГЕНЕРАТОРА ЦЕН ДЛЯ СКИНОВ CS2", "INFO")
log_message("=" * 60, "INFO")
usd_rub_rate = get_usd_to_rub_rate()
buff_prices = load_buff_prices()
skins_list = fetch_and_prepare_skins()
log_message("Начинаю генерацию вариантов...", "INFO")
final_list, missing_original = generate_full_variants(skins_list, buff_prices, usd_rub_rate)
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
json.dump(final_list, f, ensure_ascii=False, indent=2)
priced_count = sum(1 for item in final_list if item.get("price_rub") is not None)
log_message(f"Готово! Сохранено {len(final_list)} вариантов", "INFO")
log_message(f"С реальной ценой: {priced_count} ({priced_count / len(final_list) * 100:.1f}%)", "INFO")
log_message(f"Оценено: {len(final_list) - priced_count} ({(len(final_list) - priced_count) / len(final_list) * 100:.1f}%)", "INFO")
log_message(f"Файл: {OUTPUT_FILE}", "INFO")
save_missing_prices(missing_original)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
log_message("Прервано пользователем", "WARNING")
except Exception as e:
log_message(f"Критическая ошибка: {e}", "ERROR")
import traceback
traceback.print_exc()