добавлены Судьи, поправлены Ход игры и События игры на офлайн матч

This commit is contained in:
2025-10-28 12:52:50 +03:00
parent 049e2493b5
commit 94d487fe88
3 changed files with 520 additions and 12 deletions

302
PlayTypeID.json Normal file
View File

@@ -0,0 +1,302 @@
[
{
"PlayTypeID": 0,
"PlayInfoSite": "-",
"PlayInfo": "Пустое событие"
},
{
"PlayTypeID": 1,
"PlayInfoSite": "1 очко",
"PlayInfo": "Штрафной бросок - попадание"
},
{
"PlayTypeID": 2,
"PlayInfoSite": "2 очка",
"PlayInfo": "2х очковый бросок - попадание"
},
{
"PlayTypeID": 3,
"PlayInfoSite": "3 очка",
"PlayInfo": "3х очковый бросок - попадание"
},
{
"PlayTypeID": 4,
"PlayInfoSite": "Мимо 1 очко",
"PlayInfo": "Штрафной бросок - промах"
},
{
"PlayTypeID": 5,
"PlayInfoSite": "Мимо 2 очка",
"PlayInfo": "2х очковый бросок - промах"
},
{
"PlayTypeID": 6,
"PlayInfoSite": "Мимо 3 очка",
"PlayInfo": "3х очковый бросок - промах"
},
{
"PlayTypeID": 7,
"PlayInfoSite": "Замена",
"PlayInfo": "Замена игроков"
},
{
"PlayTypeID": 8,
"PlayInfoSite": "Выход",
"PlayInfo": "Выход на площадку"
},
{
"PlayTypeID": 9,
"PlayInfoSite": "Уход",
"PlayInfo": "Ушел с площадки"
},
{
"PlayTypeID": 10,
"PlayInfoSite": "Потеря",
"PlayInfo": "Потеря"
},
{
"PlayTypeID": 11,
"PlayInfoSite": "Пас-потеря",
"PlayInfo": "Потеря мяча при передаче"
},
{
"PlayTypeID": 12,
"PlayInfoSite": "Пробежка",
"PlayInfo": "Пробежка"
},
{
"PlayTypeID": 13,
"PlayInfoSite": "Заступ в аут",
"PlayInfo": "Заступ в аут"
},
{
"PlayTypeID": 14,
"PlayInfoSite": "Зона",
"PlayInfo": "Зона"
},
{
"PlayTypeID": 15,
"PlayInfoSite": "Двойное ведение",
"PlayInfo": "Двойное ведение"
},
{
"PlayTypeID": 16,
"PlayInfoSite": "3 секунды",
"PlayInfo": "3 секунды"
},
{
"PlayTypeID": 17,
"PlayInfoSite": "5 секунд",
"PlayInfo": "5 секунд"
},
{
"PlayTypeID": 18,
"PlayInfoSite": "8 секунд",
"PlayInfo": "8 секунд"
},
{
"PlayTypeID": 19,
"PlayInfoSite": "24 секунды",
"PlayInfo": "24 секунды"
},
{
"PlayTypeID": 20,
"PlayInfoSite": "Потеря мяча",
"PlayInfo": "Потеря мяча"
},
{
"PlayTypeID": 21,
"PlayInfoSite": "Начало четверти",
"PlayInfo": "Начало четверти?"
},
{
"PlayTypeID": 22,
"PlayInfoSite": "",
"PlayInfo": "Окончание четверти?"
},
{
"PlayTypeID": 23,
"PlayInfoSite": "Тайм-аут",
"PlayInfo": "Тайм-аут"
},
{
"PlayTypeID": 24,
"PlayInfoSite": "",
"PlayInfo": "неизвестно"
},
{
"PlayTypeID": 25,
"PlayInfoSite": "Пас",
"PlayInfo": "Передача"
},
{
"PlayTypeID": 26,
"PlayInfoSite": "Перехват",
"PlayInfo": "Перехват"
},
{
"PlayTypeID": 27,
"PlayInfoSite": "Блокшот",
"PlayInfo": "Блокшот"
},
{
"PlayTypeID": 28,
"PlayInfoSite": "Подбор",
"PlayInfo": "Подбор"
},
{
"PlayTypeID": 29,
"PlayInfoSite": "Быстрый отрыв",
"PlayInfo": "Быстрый отрыв"
},
{
"PlayTypeID": 30,
"PlayInfoSite": "Бросок сверху",
"PlayInfo": "Тип броска"
},
{
"PlayTypeID": 31,
"PlayInfoSite": "В прыжке",
"PlayInfo": "Тип броска"
},
{
"PlayTypeID": 32,
"PlayInfoSite": "В проходе",
"PlayInfo": "Тип броска"
},
{
"PlayTypeID": 33,
"PlayInfoSite": "Добивание",
"PlayInfo": "Тип броска"
},
{
"PlayTypeID": 34,
"PlayInfoSite": "Навес",
"PlayInfo": "Тип броска"
},
{
"PlayTypeID": 35,
"PlayInfoSite": "Бросок крюком",
"PlayInfo": "Тип броска"
},
{
"PlayTypeID": 40,
"PlayInfoSite": "Фол P",
"PlayInfo": "Фол персональный"
},
{
"PlayTypeID": 41,
"PlayInfoSite": "Фол U",
"PlayInfo": "Фол неспортивный"
},
{
"PlayTypeID": 42,
"PlayInfoSite": "Фол T",
"PlayInfo": "Фол технический"
},
{
"PlayTypeID": 43,
"PlayInfoSite": "Фол D",
"PlayInfo": "Фол дисквалифицирующий"
},
{
"PlayTypeID": 44,
"PlayInfoSite": "Фол C",
"PlayInfo": "Фол технический главному тренеру за его личное неспортивное поведение"
},
{
"PlayTypeID": 45,
"PlayInfoSite": "Фол В",
"PlayInfo": "Фол технический главному тренеру по любой другой причине"
},
{
"PlayTypeID": 47,
"PlayInfoSite": "В нападении",
"PlayInfo": "Тип фола"
},
{
"PlayTypeID": 50,
"PlayInfoSite": "Вбрасывание",
"PlayInfo": "Тип фола"
},
{
"PlayTypeID": 51,
"PlayInfoSite": "1 бросок",
"PlayInfo": "Назначение 1го штрафного броска"
},
{
"PlayTypeID": 52,
"PlayInfoSite": "2 броска",
"PlayInfo": "Назначение 2х штрафных бросков"
},
{
"PlayTypeID": 53,
"PlayInfoSite": "3 броска",
"PlayInfo": "Назначение 3х штрафных бросков"
},
{
"PlayTypeID": 54,
"PlayInfoSite": "Компенсация",
"PlayInfo": "Обоюдное пробивание штрафных"
},
{
"PlayTypeID": 59,
"PlayInfoSite": "",
"PlayInfo": "пробел для сайта"
},
{
"PlayTypeID": 60,
"PlayInfoSite": "Разминка команд",
"PlayInfo": "Разминка команд"
},
{
"PlayTypeID": 61,
"PlayInfoSite": "Представление команд",
"PlayInfo": "Представление команд"
},
{
"PlayTypeID": 62,
"PlayInfoSite": "Менее 3 минут до начала игры",
"PlayInfo": "Менее 3 минут до начала игры"
},
{
"PlayTypeID": 63,
"PlayInfoSite": "Игра сейчас начнется",
"PlayInfo": "Игра сейчас начнется"
},
{
"PlayTypeID": 69,
"PlayInfoSite": "Видеопросмотр",
"PlayInfo": "Видеопросмотр?"
},
{
"PlayTypeID": 70,
"PlayInfoSite": "",
"PlayInfo": "Возможно остановка игры?"
},
{
"PlayTypeID": 71,
"PlayInfoSite": "",
"PlayInfo": "Возможно возобновление игры?"
},
{
"PlayTypeID": 72,
"PlayInfoSite": "Видеопросмотр",
"PlayInfo": "Видеопросмотр?"
},
{
"PlayTypeID": 73,
"PlayInfoSite": "Старт видеотрансляции",
"PlayInfo": "Старт видеотрансляции"
},
{
"PlayTypeID": 74,
"PlayInfoSite": "Конец видеотрансляции",
"PlayInfo": "Конец видеотрансляции"
},
{
"PlayTypeID": 100,
"PlayInfoSite": "Матч завершен",
"PlayInfo": "Матч завершен"
}
]

View File

@@ -10,6 +10,7 @@ from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo
from typing import Any, Dict, List, Tuple, Optional
import pandas as pd
import numpy as np
import requests
from requests.adapters import HTTPAdapter
@@ -549,7 +550,7 @@ def poll_game_live(
if game_finished:
break
time.sleep(0.2)
time.sleep(1)
# вторая точка выхода по stop_event после sleep
if stop_event.is_set():
@@ -652,6 +653,8 @@ def render_loop(stop_event: threading.Event, out_name: str = "game") -> None:
Json_Team_Generation(state, who="team1")
Json_Team_Generation(state, who="team2")
Scores_Quarter(state)
Referee(state)
Play_By_Play(state)
# live_status отдельно, + общий state в <out_name>.json
atomic_write_json([state["result"]["live_status"]], "live_status")
@@ -761,7 +764,7 @@ def Referee(merged: dict, *, out_dir: str = "static") -> None:
else len(desired_order)
),
)
out_path = "referee.json"
out_path = "referee"
atomic_write_json(referees, out_path)
logging.info("Сохранил payload: {out_path}")
@@ -769,6 +772,172 @@ def Referee(merged: dict, *, out_dir: str = "static") -> None:
logger.error(f"Ошибка в Referee потоке: {e}", exc_info=True)
def Play_By_Play(data: dict) -> None:
"""
Поток, обновляющий JSON-файл с последовательностью бросков в матче.
"""
logger.info("START making json for play-by-play")
try:
game_data = data["result"] if "result" in data else data
if not game_data:
logger.debug("game_online_data отсутствует")
return
teams = game_data["teams"]
team1_data = next((i for i in teams if i.get("teamNumber") == 1), None)
team2_data = next((i for i in teams if i.get("teamNumber") == 2), None)
if not team1_data or not team2_data:
logger.warning("Не удалось получить команды из game_online_data")
return
team1_name = game_data["team1"]["name"]
team2_name = game_data["team2"]["name"]
team1_startnum = [
p["startNum"]
for p in team1_data.get("starts", [])
if p.get("startRole") == "Player"
]
team2_startnum = [
p["startNum"]
for p in team2_data.get("starts", [])
if p.get("startRole") == "Player"
]
plays = game_data.get("plays", [])
if not plays:
logger.debug("нет данных в play-by-play")
return
# Получение текущего времени игры
json_live_status = data["result"]["live_status"] if "result" in data and "live_status" in data["result"] else None
last_event = plays[-1]
# if not json_live_status or json_live_status.get("message") == "Not Found":
if json_live_status is None:
period = last_event.get("period", 1)
second = 0
else:
period = (json_live_status or {}).get("result", {}).get("period", 1)
second = (json_live_status or {}).get("result", {}).get("second", 0)
# Создание DataFrame из событий
df = pd.DataFrame(plays[::-1])
# Преобразование для лиги 3x3
# if "3x3" in LEAGUE:
# df["play"].replace({2: 1, 3: 2}, inplace=True)
df_goals = df[df["play"].isin([1, 2, 3])].copy()
if df_goals.empty:
logger.debug("нет данных о голах в play-by-play")
return
# Расчёты по очкам и времени
df_goals["score1"] = (
df_goals["startNum"].isin(team1_startnum) * df_goals["play"]
)
df_goals["score2"] = (
df_goals["startNum"].isin(team2_startnum) * df_goals["play"]
)
df_goals["score_sum1"] = df_goals["score1"].fillna(0).cumsum()
df_goals["score_sum2"] = df_goals["score2"].fillna(0).cumsum()
df_goals["new_sec"] = (
pd.to_numeric(df_goals["sec"], errors="coerce").fillna(0).astype(int)
// 10
)
df_goals["time_now"] = (600 if period < 5 else 300) - second
df_goals["quar"] = period - df_goals["period"]
df_goals["diff_time"] = np.where(
df_goals["quar"] == 0,
df_goals["time_now"] - df_goals["new_sec"],
(600 * df_goals["quar"] - df_goals["new_sec"]) + df_goals["time_now"],
)
df_goals["diff_time_str"] = df_goals["diff_time"].apply(
lambda x: (
f"{x // 60}:{str(x % 60).zfill(2)}" if isinstance(x, int) else x
)
)
# Текстовые поля
def generate_text(row, with_time=False, is_rus=False):
s1, s2 = int(row["score_sum1"]), int(row["score_sum2"])
team = (
team1_name
if not pd.isna(row["score1"]) and row["score1"] != 0
else team2_name
)
# ✅ Правильный порядок счёта в зависимости от команды
if team == team1_name:
score = f"{s1}-{s2}"
else:
score = f"{s2}-{s1}"
time_str = (
f" за {row['diff_time_str']}"
if is_rus
else f" in last {row['diff_time_str']}"
)
prefix = "рывок" if is_rus else "run"
return f"{team} {score} {prefix}{time_str if with_time else ''}"
df_goals["text_rus"] = df_goals.apply(
lambda r: generate_text(r, is_rus=True, with_time=False), axis=1
)
df_goals["text_time_rus"] = df_goals.apply(
lambda r: generate_text(r, is_rus=True, with_time=True), axis=1
)
df_goals["text"] = df_goals.apply(
lambda r: generate_text(r, is_rus=False, with_time=False), axis=1
)
df_goals["text_time"] = df_goals.apply(
lambda r: generate_text(r, is_rus=False, with_time=True), axis=1
)
df_goals["team"] = df_goals["score1"].apply(
lambda x: team1_name if not pd.isna(x) and x != 0 else team2_name
)
# Удаление лишнего
drop_cols = [
"children",
"start",
"stop",
"hl",
"sort",
"startNum",
"zone",
"x",
"y",
]
df_goals.drop(columns=drop_cols, inplace=True, errors="ignore")
# Порядок колонок
main_cols = ["text", "text_time"]
all_cols = main_cols + [
col for col in df_goals.columns if col not in main_cols
]
df_goals = df_goals[all_cols]
# Сохранение JSON
directory = "static"
os.makedirs(directory, exist_ok=True)
filepath = os.path.join(directory, "play_by_play.json")
df_goals.to_json(filepath, orient="records", force_ascii=False, indent=4)
logger.debug("Успешно положил данные об play-by-play в файл")
except Exception as e:
logger.error(f"Ошибка в Play_By_Play: {e}", exc_info=True)
def render_once_after_game(
session: requests.Session,
league: str,
@@ -808,6 +977,7 @@ def render_once_after_game(
Json_Team_Generation(state, who="team2")
Scores_Quarter(state)
Referee(state)
Play_By_Play(state)
# === 4. live_status и общий state ===
atomic_write_json(state["result"], out_name)
@@ -817,6 +987,7 @@ def render_once_after_game(
except Exception as ex:
logger.exception(f"[RENDER_ONCE] error while building final state: {ex}")
# ============================================================================
# 7. Постобработка статистики для вывода
# ============================================================================
@@ -1446,7 +1617,9 @@ def Standing_func(
# когда мы последний раз успешно обновили standings
last_call_ts = 0
json_seasons = fetch_api_data(session, "seasons", host=HOST, league=league, lang=lang)
json_seasons = fetch_api_data(
session, "seasons", host=HOST, league=league, lang=lang
)
season = json_seasons[0]["season"]
# как часто вообще можно дёргать standings
@@ -1549,7 +1722,7 @@ def Standing_func(
filename = f"standings_{league}_{comp_name}"
atomic_write_json(standings_payload, filename, out_dir)
logger.info(
f"[STANDINGS_THREAD] saved {filename}.json ({len(standings_payload)} rows)"
f"[STANDINGS_THREAD] сохранил {filename}.json"
)
# 2) плейофф-пары (playoffPairs)
@@ -1757,8 +1930,8 @@ def main():
standings_thread.join()
logger.info("[MAIN] standings thread stopped, shutdown complete")
# идём на новую итерацию while True
# (новая сессия / новый stop_event создаются в начале цикла)
# идём на новую итерацию while True
# (новая сессия / новый stop_event создаются в начале цикла)
# ============================================================================

View File

@@ -615,9 +615,7 @@ if isinstance(cached_game_online, dict):
col5_2.metric("Fouls", foulsB)
if isinstance(cached_game_online, dict) and (
cached_game_online.get("plays") or []
):
if isinstance(cached_game_online, dict) and (cached_game_online.get("plays") or []):
col_1_col = [f"col_1_{i}" for i in range(1, period_max + 1)]
col_2_col = [f"col_2_{i}" for i in range(1, period_max + 1)]
count_q = 0
@@ -1868,8 +1866,7 @@ try:
except (FileNotFoundError, json.JSONDecodeError):
play_type_id = []
teams_section = ((cached_game_online or {}).get("result") or {}).get("teams") or {}
teams_section = cached_game_online.get("teams") or {}
# Если teams_section — список (например, [{"starts": [...]}, {...}])
if isinstance(teams_section, list):
if len(teams_section) >= 2:
@@ -1882,6 +1879,7 @@ if isinstance(teams_section, list):
else:
starts1 = []
starts2 = []
# Если teams_section — словарь (обычно {"1": {...}, "2": {...}})
elif isinstance(teams_section, dict):
starts1 = (teams_section.get(1) or teams_section.get("1") or {}).get("starts") or []
@@ -1927,8 +1925,43 @@ def get_event_time(row):
return None
teams_section = cached_game_online.get("teams") or {}
# Если teams_section — список (например, [{"starts": [...]}, {...}])
if isinstance(teams_section, list):
if len(teams_section) >= 2:
starts1 = next(
(t.get("starts") for t in teams_section if t["teamNumber"] == 1), None
)
starts2 = next(
(t.get("starts") for t in teams_section if t["teamNumber"] == 2), None
)
else:
starts1 = []
starts2 = []
# Если teams_section — словарь (обычно {"1": {...}, "2": {...}})
elif isinstance(teams_section, dict):
starts1 = (teams_section.get(1) or teams_section.get("1") or {}).get("starts") or []
starts2 = (teams_section.get(2) or teams_section.get("2") or {}).get("starts") or []
else:
starts1 = []
starts2 = []
teams_temp = sorted(
[x for x in starts1 if isinstance(x, dict)], key=lambda x: x.get("playerNumber", 0)
) + sorted(
[x for x in starts2 if isinstance(x, dict)], key=lambda x: x.get("playerNumber", 0)
)
list_fullname = [None] + [
f"({x.get('displayNumber')}) {x.get('firstName','')} {x.get('lastName','')}".strip()
for x in teams_temp
if x.get("startRole") == "Player"
]
with tab_pbp:
plays = ((cached_game_online or {}).get("result") or {}).get("plays") or []
# plays = ((cached_game_online or {}).get("result") or {}).get("plays") or []
if plays:
temp_data_pbp = pd.DataFrame(plays)
col1_pbp, col2_pbp = tab_pbp.columns((3, 4))