from db import get_connection VALID_SIDES = {"home", "away"} VALID_RESULTS = {"scored", "missed"} def ensure_match_penalty_tables() -> None: """Создает таблицы для серии пенальти.""" conn = get_connection() try: with conn.cursor() as cur: cur.execute( """ CREATE TABLE IF NOT EXISTS match_penalty_settings ( match_id INTEGER PRIMARY KEY REFERENCES matches(id) ON DELETE CASCADE, max_rounds INTEGER NOT NULL DEFAULT 5, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); """ ) cur.execute( """ CREATE TABLE IF NOT EXISTS match_penalties ( id SERIAL PRIMARY KEY, match_id INTEGER NOT NULL REFERENCES matches(id) ON DELETE CASCADE, side VARCHAR(10) NOT NULL CHECK (side IN ('home', 'away')), shot_number INTEGER NOT NULL CHECK (shot_number > 0), result VARCHAR(20) NOT NULL CHECK (result IN ('scored', 'missed')), created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), UNIQUE (match_id, side, shot_number) ); """ ) conn.commit() except Exception: conn.rollback() raise finally: conn.close() def _normalize_side(side: str) -> str: side = str(side or "").strip().lower() if side not in VALID_SIDES: raise ValueError("Некорректная сторона пенальти") return side def _normalize_result(result: str) -> str: result = str(result or "").strip().lower() if result not in VALID_RESULTS: raise ValueError("Некорректный результат пенальти") return result def _ensure_penalty_rounds(cur, match_id: int, max_rounds: int) -> None: safe_rounds = max(5, int(max_rounds or 5)) cur.execute( """ INSERT INTO match_penalty_settings (match_id, max_rounds, created_at, updated_at) VALUES (%s, %s, NOW(), NOW()) ON CONFLICT (match_id) DO UPDATE SET max_rounds = GREATEST(match_penalty_settings.max_rounds, EXCLUDED.max_rounds), updated_at = NOW(); """, (match_id, safe_rounds), ) def get_penalty_state(match_id: int, home_team_name: str = "", away_team_name: str = "") -> dict: conn = get_connection() try: with conn.cursor() as cur: cur.execute( """ SELECT COALESCE(max_rounds, 5) FROM match_penalty_settings WHERE match_id = %s; """, (match_id,), ) settings_row = cur.fetchone() cur.execute( """ SELECT side, shot_number, result FROM match_penalties WHERE match_id = %s ORDER BY shot_number, side; """, (match_id,), ) rows = cur.fetchall() finally: conn.close() max_rounds = int(settings_row[0]) if settings_row else 5 for _, shot_number, _ in rows: max_rounds = max(max_rounds, int(shot_number or 0), 5) shots_map = { (str(side), int(shot_number)): str(result) for side, shot_number, result in rows } rounds = [] totals = {"home": 0, "away": 0} completed = {"home": 0, "away": 0} for number in range(1, max_rounds + 1): row = {"number": number} for side in ("home", "away"): result = shots_map.get((side, number), "") if result: completed[side] += 1 if result == "scored": totals[side] += 1 row[side] = result rounds.append(row) shots = [ {"side": str(side), "shot_number": int(shot_number), "result": str(result)} for side, shot_number, result in rows ] return { "match_id": match_id, "home_team": home_team_name or "Хозяева", "away_team": away_team_name or "Гости", "max_rounds": max_rounds, "totals": totals, "completed": completed, "rounds": rounds, "shots": shots, } def set_penalty_shot(match_id: int, side: str, shot_number: int, result: str) -> None: side = _normalize_side(side) result = _normalize_result(result) safe_shot_number = max(1, int(shot_number or 1)) conn = get_connection() try: with conn.cursor() as cur: _ensure_penalty_rounds(cur, match_id, safe_shot_number) cur.execute( """ INSERT INTO match_penalties (match_id, side, shot_number, result, created_at, updated_at) VALUES (%s, %s, %s, %s, NOW(), NOW()) ON CONFLICT (match_id, side, shot_number) DO UPDATE SET result = EXCLUDED.result, updated_at = NOW(); """, (match_id, side, safe_shot_number, result), ) conn.commit() except Exception: conn.rollback() raise finally: conn.close() def delete_penalty_shot(match_id: int, side: str, shot_number: int) -> None: side = _normalize_side(side) safe_shot_number = max(1, int(shot_number or 1)) conn = get_connection() try: with conn.cursor() as cur: cur.execute( """ DELETE FROM match_penalties WHERE match_id = %s AND side = %s AND shot_number = %s; """, (match_id, side, safe_shot_number), ) conn.commit() except Exception: conn.rollback() raise finally: conn.close() def add_penalty_round(match_id: int) -> int: conn = get_connection() try: with conn.cursor() as cur: cur.execute( """ INSERT INTO match_penalty_settings (match_id, max_rounds, created_at, updated_at) VALUES (%s, 6, NOW(), NOW()) ON CONFLICT (match_id) DO UPDATE SET max_rounds = GREATEST(match_penalty_settings.max_rounds + 1, 6), updated_at = NOW() RETURNING max_rounds; """, (match_id,), ) max_rounds = int(cur.fetchone()[0]) conn.commit() return max_rounds except Exception: conn.rollback() raise finally: conn.close() def delete_last_penalty_round(match_id: int) -> int: """Удаляет последнюю добавленную строку серии пенальти и ее данные. Первые 5 строк считаются базовыми и не удаляются. Если добавлена 6-я, 7-я и т.д. строка, удаляется самая последняя строка вместе с ударами обеих команд в этой строке. """ conn = get_connection() try: with conn.cursor() as cur: cur.execute( """ SELECT COALESCE(max_rounds, 5) FROM match_penalty_settings WHERE match_id = %s; """, (match_id,), ) settings_row = cur.fetchone() settings_max = int(settings_row[0]) if settings_row else 5 cur.execute( """ SELECT COALESCE(MAX(shot_number), 0) FROM match_penalties WHERE match_id = %s; """, (match_id,), ) shots_max = int(cur.fetchone()[0] or 0) current_max = max(5, settings_max, shots_max) if current_max <= 5: _ensure_penalty_rounds(cur, match_id, 5) conn.commit() return 5 new_max = max(5, current_max - 1) cur.execute( """ DELETE FROM match_penalties WHERE match_id = %s AND shot_number = %s; """, (match_id, current_max), ) cur.execute( """ INSERT INTO match_penalty_settings (match_id, max_rounds, created_at, updated_at) VALUES (%s, %s, NOW(), NOW()) ON CONFLICT (match_id) DO UPDATE SET max_rounds = EXCLUDED.max_rounds, updated_at = NOW(); """, (match_id, new_max), ) conn.commit() return new_max except Exception: conn.rollback() raise finally: conn.close() def clear_penalties(match_id: int) -> None: conn = get_connection() try: with conn.cursor() as cur: cur.execute("DELETE FROM match_penalties WHERE match_id = %s;", (match_id,)) cur.execute("DELETE FROM match_penalty_settings WHERE match_id = %s;", (match_id,)) conn.commit() except Exception: conn.rollback() raise finally: conn.close()