Files
WFL/repositories/match_formation_repository.py
2026-04-21 15:56:34 +03:00

370 lines
9.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from itertools import count
from db import get_connection
FORMATION_PRESETS = {
"4-4-2": {
"gk": [{"x": 50, "y": 10}],
"def": [
{"x": 18, "y": 28},
{"x": 39, "y": 24},
{"x": 61, "y": 24},
{"x": 82, "y": 28},
],
"mid": [
{"x": 18, "y": 48},
{"x": 39, "y": 44},
{"x": 61, "y": 44},
{"x": 82, "y": 48},
],
"fwd": [
{"x": 38, "y": 70},
{"x": 62, "y": 70},
],
},
"4-3-3": {
"gk": [{"x": 50, "y": 10}],
"def": [
{"x": 18, "y": 28},
{"x": 39, "y": 24},
{"x": 61, "y": 24},
{"x": 82, "y": 28},
],
"mid": [
{"x": 30, "y": 47},
{"x": 50, "y": 42},
{"x": 70, "y": 47},
],
"fwd": [
{"x": 20, "y": 72},
{"x": 50, "y": 66},
{"x": 80, "y": 72},
],
},
"4-2-3-1": {
"gk": [{"x": 0, "y": 0}],
"def": [
{"x": 18, "y": 28},
{"x": 39, "y": 24},
{"x": 61, "y": 24},
{"x": 82, "y": 28},
],
"mid": [
{"x": 35, "y": 42}, # опорник
{"x": 65, "y": 42}, # опорник
{"x": 20, "y": 58}, # левый
{"x": 50, "y": 52}, # центр
{"x": 80, "y": 58}, # правый
],
"fwd": [
{"x": 50, "y": 72},
],
},
"3-5-2": {
"gk": [{"x": 50, "y": 10}],
"def": [
{"x": 30, "y": 26},
{"x": 50, "y": 22},
{"x": 70, "y": 26},
],
"mid": [
{"x": 10, "y": 50}, # левый фланг
{"x": 35, "y": 46},
{"x": 50, "y": 42},
{"x": 65, "y": 46},
{"x": 90, "y": 50}, # правый фланг
],
"fwd": [
{"x": 38, "y": 72},
{"x": 62, "y": 72},
],
},
"5-3-2": {
"gk": [{"x": 50, "y": 10}],
"def": [
{"x": 10, "y": 30}, # левый латераль
{"x": 30, "y": 26},
{"x": 50, "y": 22},
{"x": 70, "y": 26},
{"x": 90, "y": 30}, # правый латераль
],
"mid": [
{"x": 30, "y": 48},
{"x": 50, "y": 44},
{"x": 70, "y": 48},
],
"fwd": [
{"x": 38, "y": 72},
{"x": 62, "y": 72},
],
},
}
def detect_player_line(position: str) -> str:
pos = (position or "").strip().lower()
if not pos:
return "mid"
if "вр" in pos or "gk" in pos or "goalkeeper" in pos:
return "gk"
defender_markers = ["цз", "лз", "пз", "з", "def", "cb", "lb", "rb", "wb"]
midfielder_markers = [
"цп",
"цоп",
"оп",
"п",
"пзщ",
"mid",
"cm",
"dm",
"am",
"lm",
"rm",
]
forward_markers = ["н", "цф", "ф", "lf", "rf", "fw", "st", "cf", "нап"]
if any(marker in pos for marker in defender_markers):
return "def"
if any(marker in pos for marker in midfielder_markers):
return "mid"
if any(marker in pos for marker in forward_markers):
return "fwd"
return "mid"
def get_match_formations(match_id: int, team_id: int) -> list[dict]:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT
mf.player_id,
mf.player_name,
mf.number,
mf.position,
mf.is_captain,
mf.x,
mf.y,
p.last_name
FROM match_formations mf
LEFT JOIN players p ON p.id = mf.player_id
WHERE mf.match_id = %s
AND mf.team_id = %s
ORDER BY mf.id
""",
(match_id, team_id),
)
rows = cur.fetchall()
return [
{
"player_id": row[0],
"player_name": row[1] or "",
"number": row[2] or "",
"position": row[3] or "",
"is_captain": bool(row[4]),
"x": float(row[5]),
"y": float(row[6]),
"last_name": row[7] or "",
}
for row in rows
]
finally:
conn.close()
def replace_match_formations(match_id: int, team_id: int, players: list[dict]) -> None:
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
DELETE FROM match_formations
WHERE match_id = %s AND team_id = %s
""",
(match_id, team_id),
)
for player in players:
cur.execute(
"""
INSERT INTO match_formations (
match_id,
team_id,
player_id,
player_name,
number,
position,
is_captain,
x,
y,
created_at,
updated_at
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
""",
(
match_id,
team_id,
player.get("player_id"),
(player.get("player_name") or "").strip(),
(player.get("number") or "").strip(),
(player.get("position") or "").strip(),
bool(player.get("is_captain")),
float(player.get("x", 50)),
float(player.get("y", 50)),
),
)
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def parse_position(position: str) -> tuple[str, str]:
"""
Возвращает:
line: gk / def / mid / fwd
side: left / center / right
"""
pos = (position or "").strip().lower()
if not pos:
return "mid", "center"
# Вратарь
if "вр" in pos or "gk" in pos or "goalkeeper" in pos:
return "gk", "center"
# Сторона
if pos.startswith("л"):
side = "left"
elif pos.startswith("п"):
side = "right"
else:
side = "center"
# Линия
# Защита: ЛЗ, ПЗ, ЦЗ, ЛЦЗ, ПЦЗ и т.п.
if "з" in pos:
return "def", side
# Нападение: Н, Ф, ЦФ, ЛФ, ПФ и т.п.
if "ф" in pos or "н" in pos:
return "fwd", side
# Полузащита: П, ЦП, ЦОП, ЛП, ПП, ПЦП, ЛЦП и т.п.
if "п" in pos:
return "mid", side
return "mid", side
def sort_players_by_side(players: list[dict]) -> list[dict]:
side_order = {
"left": 0,
"center": 1,
"right": 2,
}
return sorted(
players,
key=lambda p: (
side_order.get(parse_position(p.get("position", ""))[1], 1),
str(p.get("number", "")),
str(p.get("last_name", "") or p.get("player_name", "")),
),
)
def apply_formation_preset_to_players(
players: list[dict], preset_name: str
) -> list[dict]:
preset = FORMATION_PRESETS.get(preset_name)
if not preset:
raise ValueError(f"Unknown formation preset: {preset_name}")
players = players[:11]
gk, defs, mids, fwds = [], [], [], []
for p in players:
line, _ = parse_position(p.get("position"))
if line == "gk":
gk.append(p)
elif line == "def":
defs.append(p)
elif line == "mid":
mids.append(p)
elif line == "fwd":
fwds.append(p)
else:
mids.append(p)
defs = sort_players_by_side(defs)
mids = sort_players_by_side(mids)
fwds = sort_players_by_side(fwds)
leftovers = []
def trim_or_collect(group: list[dict], count: int) -> list[dict]:
if len(group) > count:
leftovers.extend(group[count:])
return group[:count]
return group
gk = trim_or_collect(gk, len(preset["gk"]))
defs = trim_or_collect(defs, len(preset["def"]))
mids = trim_or_collect(mids, len(preset["mid"]))
fwds = trim_or_collect(fwds, len(preset["fwd"]))
# если вратарь не найден — берём первого доступного
if not gk:
source = defs or mids or fwds or leftovers
if source:
gk = [source.pop(0)]
def fill(group: list[dict], count: int) -> list[dict]:
while len(group) < count and leftovers:
group.append(leftovers.pop(0))
return group
gk = fill(gk, len(preset["gk"]))
defs = fill(defs, len(preset["def"]))
mids = fill(mids, len(preset["mid"]))
fwds = fill(fwds, len(preset["fwd"]))
result = []
def assign(group: list[dict], coords: list[dict]):
for p, c in zip(group, coords):
result.append(
{
"player_id": p.get("player_id"),
"player_name": p.get("player_name") or "",
"last_name": p.get("last_name") or "",
"number": p.get("number") or "",
"position": p.get("position") or "",
"is_captain": bool(p.get("is_captain")),
"x": c["x"],
"y": c["y"],
}
)
assign(gk, preset["gk"])
assign(defs, preset["def"])
assign(mids, preset["mid"])
assign(fwds, preset["fwd"])
return result