first commit
This commit is contained in:
369
repositories/match_formation_repository.py
Normal file
369
repositories/match_formation_repository.py
Normal file
@@ -0,0 +1,369 @@
|
||||
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
|
||||
Reference in New Issue
Block a user