Enable Team Tournaments, add Tournament Trees, implement temporary tree persistance (#66)

Co-authored-by: David Rodenkirchen <drodenkirchen@linetco.com>
Reviewed-on: #66
This commit is contained in:
David Rodenkirchen 2026-04-18 14:42:28 +00:00
parent c349fe475b
commit 6666e79178
12 changed files with 466 additions and 43 deletions

View File

@ -1 +1 @@
0.3.8 0.4.0

View File

@ -1,4 +1,5 @@
import logging import logging
from uuid import uuid4
import sys import sys
@ -10,6 +11,7 @@ from from_root import from_root
from src.ezgg_lan_manager import pages, init_services, LocalDataService, RefreshService from src.ezgg_lan_manager import pages, init_services, LocalDataService, RefreshService
from src.ezgg_lan_manager.helpers.LoggedInGuard import logged_in_guard, not_logged_in_guard, team_guard from src.ezgg_lan_manager.helpers.LoggedInGuard import logged_in_guard, not_logged_in_guard, team_guard
from src.ezgg_lan_manager.services.LocalDataService import LocalData from src.ezgg_lan_manager.services.LocalDataService import LocalData
from src.ezgg_lan_manager.types.UserSession import UserSession
logger = logging.getLogger("EzggLanManager") logger = logging.getLogger("EzggLanManager")
@ -34,13 +36,13 @@ if __name__ == "__main__":
async def on_session_start(session: Session) -> None: async def on_session_start(session: Session) -> None:
# Use this line to fake being any user without having to log in # Use this line to fake being any user without having to log in
# session.attach(UserSession(id=uuid4(), user_id=30, is_team_member=True)) session.attach(UserSession(id=uuid4(), user_id=30, is_team_member=True))
await session.set_title(lan_info.name) await session.set_title(lan_info.name)
session.attach(RefreshService()) session.attach(RefreshService())
if session[LocalData].stored_session_token: # if session[LocalData].stored_session_token:
user_session = session[LocalDataService].verify_token(session[LocalData].stored_session_token) # user_session = session[LocalDataService].verify_token(session[LocalData].stored_session_token)
if user_session is not None: # if user_session is not None:
session.attach(user_session) # session.attach(user_session)
async def on_app_start(a: App) -> None: async def on_app_start(a: App) -> None:
init_result = await a.default_attachments[4].init_db_pool() init_result = await a.default_attachments[4].init_db_pool()
@ -176,6 +178,11 @@ if __name__ == "__main__":
url_segment="tournament", url_segment="tournament",
build=pages.TournamentDetailsPage, build=pages.TournamentDetailsPage,
), ),
ComponentPage(
name="TournamentTreePage",
url_segment="tournament-tree",
build=pages.TournamentTreePage,
),
ComponentPage( ComponentPage(
name="TournamentRulesPage", name="TournamentRulesPage",
url_segment="tournament-rules", url_segment="tournament-rules",

View File

@ -11,7 +11,7 @@ from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBo
from src.ezgg_lan_manager.types.DateUtil import weekday_to_display_text from src.ezgg_lan_manager.types.DateUtil import weekday_to_display_text
from src.ezgg_lan_manager.types.Participant import Participant from src.ezgg_lan_manager.types.Participant import Participant
from src.ezgg_lan_manager.types.Tournament import Tournament from src.ezgg_lan_manager.types.Tournament import Tournament
from src.ezgg_lan_manager.types.TournamentBase import TournamentStatus from src.ezgg_lan_manager.types.TournamentBase import TournamentStatus, TournamentError
logger = logging.getLogger(__name__.split(".")[-1]) logger = logging.getLogger(__name__.split(".")[-1])
@ -29,7 +29,10 @@ class ManageTournamentsPage(Component):
async def on_start_pressed(self, tournament_id: int) -> None: async def on_start_pressed(self, tournament_id: int) -> None:
logger.info(f"Starting tournament with ID {tournament_id}") logger.info(f"Starting tournament with ID {tournament_id}")
await self.session[TournamentService].start_tournament(tournament_id) try:
await self.session[TournamentService].start_tournament(tournament_id)
except TournamentError as e:
logger.error(f"Error trying to start tournament: {e}")
async def on_cancel_pressed(self, tournament_id: int) -> None: async def on_cancel_pressed(self, tournament_id: int) -> None:
logger.info(f"Canceling tournament with ID {tournament_id}") logger.info(f"Canceling tournament with ID {tournament_id}")
@ -92,9 +95,17 @@ class ManageTournamentsPage(Component):
font_size=1.2 font_size=1.2
), ),
margin_top=2, margin_top=2,
margin_bottom=2, margin_bottom=1,
align_x=0.5 align_x=0.5
), ),
Button(
content="Cache erneuern",
shape="rectangle",
style="colored-text",
margin_bottom=2,
align_x=0.5,
on_press=self.session[TournamentService].queue_cache_renewal
),
*tournament_rows *tournament_rows
) )
), ),

View File

@ -7,13 +7,13 @@ from from_root import from_root
from rio import Column, Component, event, TextStyle, Text, Row, Image, Spacer, ProgressCircle, Button, Checkbox, ThemeContextSwitcher, Link, Revealer, PointerEventListener, \ from rio import Column, Component, event, TextStyle, Text, Row, Image, Spacer, ProgressCircle, Button, Checkbox, ThemeContextSwitcher, Link, Revealer, PointerEventListener, \
PointerEvent, Rectangle, Color, Popup, Dropdown PointerEvent, Rectangle, Color, Popup, Dropdown
from src.ezgg_lan_manager import ConfigurationService, TournamentService, UserService, TicketingService, TeamService from src.ezgg_lan_manager import ConfigurationService, TournamentService, UserService, TicketingService, TeamService, RefreshService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.components.TournamentDetailsInfoRow import TournamentDetailsInfoRow from src.ezgg_lan_manager.components.TournamentDetailsInfoRow import TournamentDetailsInfoRow
from src.ezgg_lan_manager.types.DateUtil import weekday_to_display_text from src.ezgg_lan_manager.types.DateUtil import weekday_to_display_text
from src.ezgg_lan_manager.types.Team import Team, TeamStatus from src.ezgg_lan_manager.types.Team import Team, TeamStatus
from src.ezgg_lan_manager.types.Tournament import Tournament from src.ezgg_lan_manager.types.Tournament import Tournament
from src.ezgg_lan_manager.types.TournamentBase import TournamentStatus, tournament_status_to_display_text, tournament_format_to_display_texts, ParticipantType from src.ezgg_lan_manager.types.TournamentBase import TournamentStatus, tournament_status_to_display_text, tournament_format_to_display_texts, ParticipantType, TournamentFormat
from src.ezgg_lan_manager.types.User import User from src.ezgg_lan_manager.types.User import User
from src.ezgg_lan_manager.types.UserSession import UserSession from src.ezgg_lan_manager.types.UserSession import UserSession
@ -44,7 +44,7 @@ class TournamentDetailsPage(Component):
tournament_id = None tournament_id = None
if tournament_id is not None: if tournament_id is not None:
self.tournament = await self.session[TournamentService].get_tournament_by_id(tournament_id) self.tournament = await self.session[TournamentService].get_tournament_by_id(tournament_id)
if self.tournament is not None: if isinstance(self.tournament, Tournament):
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - {self.tournament.name}") await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - {self.tournament.name}")
if self.tournament.participant_type == ParticipantType.PLAYER: if self.tournament.participant_type == ParticipantType.PLAYER:
self.current_tournament_user_or_team_list = await self.session[TournamentService].get_users_from_participant_list(self.tournament.participants) self.current_tournament_user_or_team_list = await self.session[TournamentService].get_users_from_participant_list(self.tournament.participants)
@ -61,6 +61,8 @@ class TournamentDetailsPage(Component):
self.user = None self.user = None
self.user_teams = [] self.user_teams = []
self.session[RefreshService].subscribe(self.on_populate)
self.loading_done() self.loading_done()
@staticmethod @staticmethod
@ -68,13 +70,14 @@ class TournamentDetailsPage(Component):
await sleep(0.8) # https://medium.com/design-bootcamp/ux-psychology-of-artificial-waiting-enhancing-user-experiences-through-deliberate-delays-d7822faf3930 await sleep(0.8) # https://medium.com/design-bootcamp/ux-psychology-of-artificial-waiting-enhancing-user-experiences-through-deliberate-delays-d7822faf3930
async def update(self) -> None: async def update(self) -> None:
self.tournament = await self.session[TournamentService].get_tournament_by_id(self.tournament.id) if isinstance(self.tournament, Tournament):
if self.tournament is None: self.tournament = await self.session[TournamentService].get_tournament_by_id(self.tournament.id)
return if self.tournament is None or isinstance(self.tournament, str):
if self.tournament.participant_type == ParticipantType.PLAYER: return
self.current_tournament_user_or_team_list = await self.session[TournamentService].get_users_from_participant_list(self.tournament.participants) if self.tournament.participant_type == ParticipantType.PLAYER:
elif self.tournament.participant_type == ParticipantType.TEAM: self.current_tournament_user_or_team_list = await self.session[TournamentService].get_users_from_participant_list(self.tournament.participants)
self.current_tournament_user_or_team_list = await self.session[TournamentService].get_teams_from_participant_list(self.tournament.participants) elif self.tournament.participant_type == ParticipantType.TEAM:
self.current_tournament_user_or_team_list = await self.session[TournamentService].get_teams_from_participant_list(self.tournament.participants)
def open_close_participant_revealer(self, _: PointerEvent) -> None: def open_close_participant_revealer(self, _: PointerEvent) -> None:
self.participant_revealer_open = not self.participant_revealer_open self.participant_revealer_open = not self.participant_revealer_open
@ -88,6 +91,9 @@ class TournamentDetailsPage(Component):
if user_ticket is None: if user_ticket is None:
self.is_success = False self.is_success = False
self.message = "Turnieranmeldung nur mit Ticket" self.message = "Turnieranmeldung nur mit Ticket"
elif not isinstance(self.tournament, Tournament):
self.is_success = False
self.message = "Fehler bei der Anmeldung"
else: else:
# Register single player # Register single player
if self.tournament.participant_type == ParticipantType.PLAYER: if self.tournament.participant_type == ParticipantType.PLAYER:
@ -125,12 +131,15 @@ class TournamentDetailsPage(Component):
await self.on_team_register_canceled() await self.on_team_register_canceled()
return return
try: try:
await self.session[TournamentService].register_team_for_tournament(self.team_selected_for_register.id, self.tournament.id) if isinstance(self.tournament, Tournament):
await self.artificial_delay() await self.session[TournamentService].register_team_for_tournament(self.team_selected_for_register.id, self.tournament.id)
self.is_success = True await self.artificial_delay()
self.message = f"Erfolgreich angemeldet!" self.is_success = True
self.team_dialog_open = False self.message = f"Erfolgreich angemeldet!"
self.team_selected_for_register = None self.team_dialog_open = False
self.team_selected_for_register = None
else:
raise ValueError("Turnier nicht gefunden")
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
self.message = f"Fehler: {e}" self.message = f"Fehler: {e}"
@ -149,9 +158,9 @@ class TournamentDetailsPage(Component):
return return
try: try:
if self.tournament.participant_type == ParticipantType.PLAYER: if isinstance(self.tournament, Tournament) and self.tournament.participant_type == ParticipantType.PLAYER:
await self.session[TournamentService].unregister_user_from_tournament(self.user.user_id, self.tournament.id) await self.session[TournamentService].unregister_user_from_tournament(self.user.user_id, self.tournament.id)
elif self.tournament.participant_type == ParticipantType.TEAM: elif isinstance(self.tournament, Tournament) and self.tournament.participant_type == ParticipantType.TEAM:
if team.members[self.user] == TeamStatus.OFFICER or team.members[self.user] == TeamStatus.LEADER: if team.members[self.user] == TeamStatus.OFFICER or team.members[self.user] == TeamStatus.LEADER:
await self.session[TournamentService].unregister_team_from_tournament(team.id, self.tournament.id) await self.session[TournamentService].unregister_team_from_tournament(team.id, self.tournament.id)
else: else:
@ -166,7 +175,8 @@ class TournamentDetailsPage(Component):
self.loading = False self.loading = False
async def tree_button_clicked(self) -> None: async def tree_button_clicked(self) -> None:
pass # ToDo: Implement tournament tree view if isinstance(self.tournament, Tournament):
self.session.navigate_to(f"tournament-tree?id={self.tournament.id}")
def loading_done(self) -> None: def loading_done(self) -> None:
if self.tournament is None: if self.tournament is None:
@ -205,16 +215,17 @@ class TournamentDetailsPage(Component):
tournament_status_color = self.session.theme.danger_color tournament_status_color = self.session.theme.danger_color
elif self.tournament.status == TournamentStatus.ONGOING or self.tournament.status == TournamentStatus.COMPLETED: elif self.tournament.status == TournamentStatus.ONGOING or self.tournament.status == TournamentStatus.COMPLETED:
tournament_status_color = self.session.theme.warning_color tournament_status_color = self.session.theme.warning_color
tree_button = Button( if self.tournament.format != TournamentFormat.FFA:
content="Turnierbaum anzeigen", tree_button = Button(
shape="rectangle", content="Turnierbaum anzeigen",
style="minor", shape="rectangle",
color="hud", style="minor",
margin_left=4, color="hud",
margin_right=4, margin_left=4,
margin_top=1, margin_right=4,
on_press=self.tree_button_clicked margin_top=1,
) on_press=self.tree_button_clicked
)
ids_of_participants = [p.id for p in self.tournament.participants] ids_of_participants = [p.id for p in self.tournament.participants]
color_key: Literal["hud", "danger"] = "hud" color_key: Literal["hud", "danger"] = "hud"
@ -349,7 +360,7 @@ class TournamentDetailsPage(Component):
button button
) )
if self.tournament and self.tournament.participant_type == ParticipantType.TEAM: if isinstance(self.tournament, Tournament) and self.tournament.participant_type == ParticipantType.TEAM:
content = Popup( content = Popup(
anchor=content, anchor=content,
content=Rectangle( content=Rectangle(

View File

@ -0,0 +1,280 @@
import json
import logging
from typing import Optional, Union
from from_root import from_root
from rio import Column, Component, event, TextStyle, Text, Row, Spacer, ProgressCircle, Rectangle, Stack
from src.ezgg_lan_manager import ConfigurationService, TournamentService, UserService, TeamService, RefreshService, SeatingService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.types.Team import Team, TeamStatus
from src.ezgg_lan_manager.types.Tournament import Tournament
from src.ezgg_lan_manager.types.TournamentBase import ParticipantType, TournamentFormat
from src.ezgg_lan_manager.types.User import User
from src.ezgg_lan_manager.types.UserSession import UserSession
logger = logging.getLogger(__name__.split(".")[-1])
class MatchInfo(Component):
opponent_1: str = ""
opponent_2: str = ""
opponent_1_seat: str = ""
opponent_2_seat: str = ""
winner: str = ""
def build(self) -> Component:
return Rectangle(
content=Column(
Stack(
Row(
Row(
Text(
text=self.opponent_1,
style=TextStyle(fill=self.session.theme.success_color if self.winner == self.opponent_1 else self.session.theme.background_color),
justify="left",
margin_right=0.6,
font_size=0.9
),
Text(
text=f"({self.opponent_1_seat})" if self.opponent_1_seat else "Freilos",
style=TextStyle(fill=self.session.theme.background_color),
justify="left",
font_size=0.9
)
),
Spacer(),
Row(
Text(
text=self.opponent_2,
style=TextStyle(fill=self.session.theme.success_color if self.winner == self.opponent_2 else self.session.theme.background_color),
justify="right",
margin_right=0.6,
font_size=0.9
),
Text(
text=f"({self.opponent_2_seat})" if self.opponent_2_seat else "Freilos",
style=TextStyle(fill=self.session.theme.background_color),
justify="right",
font_size=0.9
)
),
margin=0.3
),
Row(
Text(
text=f"vs.",
style=TextStyle(fill=self.session.theme.background_color),
justify="center"
),
margin=0.3
)
)
),
margin=1,
stroke_width=0.2,
stroke_color=self.session.theme.background_color,
fill=self.session.theme.hud_color,
)
class TournamentTreePage(Component):
tournament: Optional[Union[Tournament, str]] = None
user: Optional[User] = None
teams: list[Team] = []
id_to_username_map: dict[int, str] = {}
id_to_seat_map: dict[int, str] = {}
is_fully_loaded: bool = False
@event.on_populate
async def on_populate(self) -> None:
try:
tournament_id = int(self.session.active_page_url.query_string.split("id=")[-1])
except (ValueError, AttributeError, TypeError):
tournament_id = None
if tournament_id is not None:
self.tournament = await self.session[TournamentService].get_tournament_by_id(tournament_id)
if isinstance(self.tournament, Tournament):
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - {self.tournament.name}")
else:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Turniere")
try:
user_id = self.session[UserSession].user_id
self.user = await self.session[UserService].get_user(user_id)
except KeyError:
self.user = None
self.teams = await self.session[TeamService].get_all_teams()
all_users = await self.session[UserService].get_all_users()
id_to_username_map = {}
id_to_seat_map = {}
for user in all_users:
id_to_username_map[user.user_id] = user.user_name
seat = await self.session[SeatingService].get_user_seat(user.user_id)
if seat is not None:
id_to_seat_map[user.user_id] = seat.seat_id
self.id_to_username_map = id_to_username_map
self.id_to_seat_map = id_to_seat_map
self.session[RefreshService].subscribe(self.on_populate)
self.is_fully_loaded = True
def _get_seat_for_team(self, team: Team) -> str:
# Retrieves seat id for leader of a team
leader = list(team.members.keys())[0]
for member, rank in team.members.items():
if rank == TeamStatus.LEADER:
leader = member
break
return self.id_to_seat_map[leader.user_id]
def build(self) -> Component:
if self.tournament is None or not self.is_fully_loaded:
return Column(
MainViewContentBox(
Column(
Spacer(min_height=1),
Column(
ProgressCircle(
color="secondary",
align_x=0.5,
margin_top=0,
margin_bottom=0
),
min_height=10
),
Spacer(min_height=1)
)
),
align_y=0
)
elif isinstance(self.tournament, str):
content = Row(
Text(
text=self.tournament,
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1
),
margin_top=2,
margin_bottom=2,
align_x=0.5
)
)
else:
if self.tournament.format == TournamentFormat.FFA:
content = Column(
Text(
text=f"Dieses Turnier hat keinen Turnierbaum.",
style=TextStyle(fill=self.session.theme.background_color),
margin_top=1,
margin_bottom=1,
align_x=0.5,
overflow="wrap",
min_width=30,
justify="center"
)
)
else:
try:
file_name = self.tournament.name.replace(" ", "_") + ".json"
games_per_matchup = int(self.tournament.format.name[-1])
logger.info(f"Trying to read tournament data from {file_name}")
with open(from_root("tournament_data", file_name), "r") as f:
json_data = json.load(f)
last_valid_round = None
round_num = 0
for round_ in json_data["rounds"]:
if all(
match["opponent_1_id"] is not None or match["opponent_2_id"] is not None
for match in round_
):
last_valid_round = round_
round_num += 1
if last_valid_round is None:
last_valid_round = json_data["rounds"][0]
match_infos = []
if self.tournament.participant_type == ParticipantType.PLAYER:
match_infos = [MatchInfo(
opponent_1=self.id_to_username_map.get(match["opponent_1_id"], ""),
opponent_2=self.id_to_username_map.get(match["opponent_2_id"], ""),
winner=self.id_to_username_map.get(match["winner"], ""),
opponent_1_seat=self.id_to_seat_map.get(match["opponent_1_id"], ""),
opponent_2_seat=self.id_to_seat_map.get(match["opponent_2_id"], ""),
) for match in last_valid_round]
elif self.tournament.participant_type == ParticipantType.TEAM:
for match in last_valid_round:
team_1: Optional[Team] = next(filter(lambda t: t.id == match["opponent_1_id"], self.teams), None)
team_2: Optional[Team] = next(filter(lambda t: t.id == match["opponent_2_id"], self.teams), None)
winner: Union[str, Team] = next(filter(lambda t: t.id == match["winner"], self.teams), "")
match_infos.append(
MatchInfo(
opponent_1=team_1.name if team_1 is not None else "",
opponent_2=team_2.name if team_2 is not None else "",
winner=winner if isinstance(winner, str) else winner.name,
opponent_1_seat=self._get_seat_for_team(team_1) if team_1 is not None else "",
opponent_2_seat=self._get_seat_for_team(team_2) if team_2 is not None else "",
)
)
else:
raise ValueError("Unknown participant type")
content = Column(
Text(
text=f"{self.tournament.name}",
style=TextStyle(fill=self.session.theme.background_color),
justify="center",
font_size=1.2
),
Text(
text="Finale" if len(json_data["rounds"]) == round_num else f"Runde {round_num}",
style=TextStyle(fill=self.session.theme.background_color),
justify="center",
font_size=0.9,
margin_bottom=1
),
Text(
text=f"Spiele pro Matchup: {games_per_matchup}",
style=TextStyle(fill=self.session.theme.background_color),
justify="center",
font_size=0.8
),
Text(
text=f"Melde als Verlierer deinen Matchausgang\nim Discord oder an der Orga-Ecke",
style=TextStyle(fill=self.session.theme.background_color),
justify="center",
font_size=0.8
),
*match_infos
)
except (FileNotFoundError, ValueError, AttributeError):
content = Column(
Text(
text=f"Der Turnierbaum für dieses Turnier steht leider nicht zur Verfügung.\n\nBitte melde sich beim Orga-Team.",
style=TextStyle(fill=self.session.theme.background_color),
margin_top=1,
margin_bottom=1,
align_x=0.5,
overflow="wrap",
min_width=30,
justify="center"
)
)
return Column(
MainViewContentBox(
Column(
Spacer(min_height=1),
content,
Spacer(min_height=1)
)
),
align_y=0
)

View File

@ -25,3 +25,4 @@ from .TournamentRulesPage import TournamentRulesPage
from .ConwayPage import ConwayPage from .ConwayPage import ConwayPage
from .TeamsPage import TeamsPage from .TeamsPage import TeamsPage
from .AdminNavigationPage import AdminNavigationPage from .AdminNavigationPage import AdminNavigationPage
from .TournamentTreePage import TournamentTreePage

View File

@ -1,6 +1,6 @@
import logging import logging
from datetime import date, datetime from datetime import date, datetime, UTC
from typing import Optional from typing import Optional
from decimal import Decimal from decimal import Decimal
@ -16,7 +16,7 @@ from src.ezgg_lan_manager.types.Seat import Seat
from src.ezgg_lan_manager.types.Team import TeamStatus, Team from src.ezgg_lan_manager.types.Team import TeamStatus, Team
from src.ezgg_lan_manager.types.Ticket import Ticket from src.ezgg_lan_manager.types.Ticket import Ticket
from src.ezgg_lan_manager.types.Tournament import Tournament from src.ezgg_lan_manager.types.Tournament import Tournament
from src.ezgg_lan_manager.types.TournamentBase import GameTitle, TournamentFormat, TournamentStatus, ParticipantType from src.ezgg_lan_manager.types.TournamentBase import GameTitle, TournamentFormat, TournamentStatus, ParticipantType, MatchStatus
from src.ezgg_lan_manager.types.Transaction import Transaction from src.ezgg_lan_manager.types.Transaction import Transaction
from src.ezgg_lan_manager.types.User import User from src.ezgg_lan_manager.types.User import User
@ -1185,3 +1185,18 @@ class DatabaseService:
if not pool_init_result: if not pool_init_result:
raise NoDatabaseConnectionError raise NoDatabaseConnectionError
return await self.remove_user_from_team(team, user) return await self.remove_user_from_team(team, user)
async def change_tournament_status(self, tournament_id: int, status: TournamentStatus) -> None:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute(
"UPDATE tournaments SET status = %s WHERE (id = %s)",
(status.name, tournament_id)
)
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.change_tournament_status(tournament_id, status)

View File

@ -1,5 +1,9 @@
import json
from pprint import pprint
from typing import Optional from typing import Optional
from from_root import from_root
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
from src.ezgg_lan_manager.services.UserService import UserService from src.ezgg_lan_manager.services.UserService import UserService
from src.ezgg_lan_manager.types.Participant import Participant from src.ezgg_lan_manager.types.Participant import Participant
@ -18,6 +22,10 @@ class TournamentService:
self._cache: dict[int, Tournament] = {} self._cache: dict[int, Tournament] = {}
self._cache_dirty: bool = True # Setting this flag invokes cache update on next read self._cache_dirty: bool = True # Setting this flag invokes cache update on next read
async def queue_cache_renewal(self) -> None:
# Used in admin UI to provoke cache renewal after direct database access
self._cache_dirty = True
async def _update_cache(self) -> None: async def _update_cache(self) -> None:
tournaments = await self._db_service.get_all_tournaments() tournaments = await self._db_service.get_all_tournaments()
for tournament in tournaments: for tournament in tournaments:
@ -90,12 +98,47 @@ class TournamentService:
tournament = await self.get_tournament_by_id(tournament_id) tournament = await self.get_tournament_by_id(tournament_id)
if tournament: if tournament:
tournament.start() tournament.start()
# ToDo: Write matches/round to database await self._generate_initial_json_file(tournament)
await self._db_service.change_tournament_status(tournament_id, tournament.status)
self._cache_dirty = True self._cache_dirty = True
async def cancel_tournament(self, tournament_id: int): async def cancel_tournament(self, tournament_id: int):
tournament = await self.get_tournament_by_id(tournament_id) tournament = await self.get_tournament_by_id(tournament_id)
if tournament: if tournament:
tournament.cancel() tournament.cancel()
# ToDo: Update to database await self._db_service.change_tournament_status(tournament_id, tournament.status)
self._cache_dirty = True self._cache_dirty = True
async def _generate_initial_json_file(self, tournament: Tournament) -> None:
"""
Generates the initial JSON file for the tournament. Won't generate a new one if one already exists.
ToDo: Remove this method when final tournament system is completed.
"""
p = tournament.participants
pairs = [
(p[i], p[i + 1]) if i + 1 < len(p) else (p[i], None)
for i in range(0, len(p), 2)
]
data = {
"rounds": [
[
{
"opponent_1_id": pair[0].id if pair[0] is not None else None,
"opponent_2_id": pair[1].id if pair[1] is not None else None,
"winner": None
} for pair in pairs
]
]
}
# Resolve byes
for match in data["rounds"][0]:
if match["opponent_2_id"] is None:
match["winner"] = match["opponent_1_id"]
file_name = tournament.name.replace(" ", "_") + ".json"
try:
with open(from_root("tournament_data", file_name), "x") as f:
json.dump(data, f, indent=4)
except FileExistsError:
pass

View File

@ -49,6 +49,14 @@ class Match:
games.append(Game(game_id, self._match_id, game_number, None, None, False)) games.append(Game(game_id, self._match_id, game_number, None, None, False))
return tuple(games) return tuple(games)
@property
def round_number(self) -> int:
return self._round_number
@property
def best_of(self) -> int:
return self._best_of
@property @property
def status(self) -> MatchStatus: def status(self) -> MatchStatus:
if self._status == MatchStatus.COMPLETED: if self._status == MatchStatus.COMPLETED:

View File

@ -1,3 +1,4 @@
import logging
import uuid import uuid
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
@ -7,6 +8,7 @@ from src.ezgg_lan_manager.types.Match import Match, FFAMatch
from src.ezgg_lan_manager.types.Participant import Participant from src.ezgg_lan_manager.types.Participant import Participant
from src.ezgg_lan_manager.types.TournamentBase import GameTitle, TournamentFormat, TournamentStatus, TournamentError, Bracket, MatchStatus, ParticipantType from src.ezgg_lan_manager.types.TournamentBase import GameTitle, TournamentFormat, TournamentStatus, TournamentError, Bracket, MatchStatus, ParticipantType
logger = logging.getLogger(__name__.split(".")[-1])
class Tournament: class Tournament:
def __init__(self, def __init__(self,
@ -353,6 +355,7 @@ class Tournament:
raise TournamentError(f"Unknown bracket type: {bracket_type}") raise TournamentError(f"Unknown bracket type: {bracket_type}")
self._status = TournamentStatus.ONGOING self._status = TournamentStatus.ONGOING
logger.info(f"New tournament status for {self._name}: {self._status}")
for match in self._matches: for match in self._matches:
match.check_completion() match.check_completion()

1
tournament_data/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.json

43
tournament_data/README.md Normal file
View File

@ -0,0 +1,43 @@
# Tournament data
This directory contains JSON files for tournament trees.
This is a temporary solution until the automatic tournament tree generation is completed.
# Structure
## Naming
Tournament name with `_` as separators and `.json` suffix.
## JSON structure
```json
{
"rounds": [
[
{
"opponent_1_id": 1,
"opponent_2_id": 2,
"winner": 1
},
{
"opponent_1_id": 3,
"opponent_2_id": 4,
"winner": null
}
],
[
{
"opponent_1_id": 1,
"opponent_2_id": null,
"winner": null
}
]
]
}
```
## ToDo
- Make start button in UI generate initial `.json` file for started tournament