372 lines
10 KiB
Python
372 lines
10 KiB
Python
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,
|
||
p.position as position_full
|
||
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 "",
|
||
"position_full": row[8] 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
|