Compare commits

..

No commits in common. "e386d89c6f10e111ff3de13f5c8a9c9fa13604d9" and "80b6e49d388f5e181aa63f434b95bb93e05db370" have entirely different histories.

7 changed files with 22 additions and 195 deletions

View File

@ -1,117 +1,32 @@
import logging import logging
from datetime import datetime
from typing import Optional
from from_root import from_root from rio import Column, Component, event, TextStyle, Text, Spacer
from rio import Column, Component, event, TextStyle, Text, Spacer, Row, Image, Tooltip, IconButton, Popup, Rectangle, Dropdown, ThemeContextSwitcher, Button
from src.ezgg_lan_manager import ConfigurationService, TournamentService, UserService from src.ezgg_lan_manager import ConfigurationService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
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.Tournament import Tournament
from src.ezgg_lan_manager.types.TournamentBase import TournamentStatus
logger = logging.getLogger(__name__.split(".")[-1]) logger = logging.getLogger(__name__.split(".")[-1])
class ManageTournamentsPage(Component): class ManageTournamentsPage(Component):
tournaments: list[Tournament] = []
remove_participant_popup_open: bool = False
cancel_options: dict[str, Optional[Participant]] = {"": None}
tournament_id_selected_for_participant_removal: Optional[int] = None
participant_selected_for_removal: Optional[Participant] = None
@event.on_populate @event.on_populate
async def on_populate(self) -> None: async def on_populate(self) -> None:
self.tournaments = await self.session[TournamentService].get_tournaments()
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Turnier Verwaltung") await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Turnier Verwaltung")
async def on_start_pressed(self, tournament_id: int) -> None:
logger.info(f"Starting tournament with ID {tournament_id}")
await self.session[TournamentService].start_tournament(tournament_id)
async def on_cancel_pressed(self, tournament_id: int) -> None:
logger.info(f"Canceling tournament with ID {tournament_id}")
await self.session[TournamentService].cancel_tournament(tournament_id)
async def on_remove_participant_pressed(self, tournament_id: int) -> None:
tournament = await self.session[TournamentService].get_tournament_by_id(tournament_id)
if tournament is None:
return
users = await self.session[UserService].get_all_users()
try:
self.cancel_options = {next(filter(lambda u: u.user_id == p.id, users)).user_name: p for p in tournament.participants}
except StopIteration as e:
logger.error(f"Error trying to find user for participant: {e}")
self.tournament_id_selected_for_participant_removal = tournament_id
self.remove_participant_popup_open = True
async def on_remove_participant_confirm_pressed(self) -> None:
if self.participant_selected_for_removal is not None and self.tournament_id_selected_for_participant_removal is not None:
logger.info(f"Removing participant with ID {self.participant_selected_for_removal.id} from tournament with ID {self.tournament_id_selected_for_participant_removal}")
await self.session[TournamentService].unregister_user_from_tournament(self.participant_selected_for_removal.id, self.tournament_id_selected_for_participant_removal)
await self.on_remove_participant_cancel_pressed()
async def on_remove_participant_cancel_pressed(self) -> None:
self.tournament_id_selected_for_participant_removal = None
self.participant_selected_for_removal = None
self.remove_participant_popup_open = False
def build(self) -> Component: def build(self) -> Component:
tournament_rows = []
for tournament in self.tournaments:
start_time_color = self.session.theme.background_color
if tournament.start_time < datetime.now() and tournament.status == TournamentStatus.OPEN:
start_time_color = self.session.theme.warning_color
tournament_rows.append(
Row(
Image(image=from_root(f"src/ezgg_lan_manager/assets/img/games/{tournament.game_title.image_name}"), min_width=1.5, margin_right=1),
Text(tournament.name, style=TextStyle(fill=self.session.theme.background_color, font_size=0.8), justify="left", margin_right=1.5),
Text(f"{weekday_to_display_text(tournament.start_time.weekday())[:2]}.{tournament.start_time.strftime('%H:%M')} Uhr", style=TextStyle(fill=start_time_color, font_size=0.8), justify="left", margin_right=1),
Spacer(),
Tooltip(anchor=IconButton("material/play_arrow", min_size=2, margin_right=1, on_press=lambda: self.on_start_pressed(tournament.id)), tip="Starten"),
Tooltip(anchor=IconButton("material/cancel_schedule_send", min_size=2, margin_right=1, on_press=lambda: self.on_cancel_pressed(tournament.id)), tip="Absagen"),
Tooltip(anchor=IconButton("material/person_cancel", min_size=2, margin_right=1, on_press=lambda: self.on_remove_participant_pressed(tournament.id)), tip="Spieler entfernen"),
margin=1
)
)
return Column( return Column(
Popup( MainViewContentBox(
anchor=MainViewContentBox( Column(
Column( Text(
Text( text="Turnier Verwaltung",
text="Turnier Verwaltung", style=TextStyle(
style=TextStyle( fill=self.session.theme.background_color,
fill=self.session.theme.background_color, font_size=1.2
font_size=1.2
),
margin_top=2,
margin_bottom=2,
align_x=0.5
), ),
*tournament_rows margin_top=2,
margin_bottom=2,
align_x=0.5
) )
), )
content=Rectangle(
content=Row(
ThemeContextSwitcher(
content=Dropdown(options=self.cancel_options, min_width=20, selected_value=self.bind().participant_selected_for_removal), color=self.session.theme.hud_color
),
Button(content="REMOVE", shape="rectangle", grow_x=False, on_press=self.on_remove_participant_confirm_pressed),
Button(content="CANCEL", shape="rectangle", grow_x=False, on_press=self.on_remove_participant_cancel_pressed),
margin=0.5
),
min_width=30,
min_height=4,
fill=self.session.theme.primary_color,
margin_top=3.5,
stroke_width=0.3,
stroke_color=self.session.theme.neutral_color,
),
is_open=self.remove_participant_popup_open,
color="none"
), ),
Spacer() Spacer()
) )

View File

@ -99,8 +99,6 @@ class DatabaseService:
return TournamentFormat.DOUBLE_ELIMINATION_BO_3 return TournamentFormat.DOUBLE_ELIMINATION_BO_3
elif format_as_string == "DE_BO_5": elif format_as_string == "DE_BO_5":
return TournamentFormat.DOUBLE_ELIMINATION_BO_5 return TournamentFormat.DOUBLE_ELIMINATION_BO_5
elif format_as_string == "FFA":
return TournamentFormat.FFA
else: else:
# If this happens, database is FUBAR # If this happens, database is FUBAR
raise RuntimeError(f"Unknown TournamentFormat: {format_as_string}") raise RuntimeError(f"Unknown TournamentFormat: {format_as_string}")

View File

@ -56,17 +56,3 @@ class TournamentService:
all_users = await self._db_service.get_all_users() all_users = await self._db_service.get_all_users()
participant_ids = [p.id for p in participants] participant_ids = [p.id for p in participants]
return list(filter(lambda u: u.user_id in participant_ids, all_users)) return list(filter(lambda u: u.user_id in participant_ids, all_users))
async def start_tournament(self, tournament_id: int):
tournament = await self.get_tournament_by_id(tournament_id)
if tournament:
tournament.start()
# ToDo: Write matches/round to database
self._cache_dirty = True
async def cancel_tournament(self, tournament_id: int):
tournament = await self.get_tournament_by_id(tournament_id)
if tournament:
tournament.cancel()
# ToDo: Update to database
self._cache_dirty = True

View File

@ -8,9 +8,9 @@ from src.ezgg_lan_manager.types.TournamentBase import MatchStatus, TournamentErr
class MatchParticipant: class MatchParticipant:
def __init__(self, participant_id: int, slot_number: Literal[-1, 1, 2]) -> None: def __init__(self, participant_id: int, slot_number: Literal[1, 2]) -> None:
self._participant_id = participant_id self._participant_id = participant_id
if slot_number not in (-1, 1, 2): if slot_number not in (1, 2):
raise TournamentError("Invalid slot number") raise TournamentError("Invalid slot number")
self.slot_number = slot_number self.slot_number = slot_number
@ -99,9 +99,7 @@ class Match:
def next_match_lose_id(self) -> Optional[int]: def next_match_lose_id(self) -> Optional[int]:
return self._next_match_lose_id return self._next_match_lose_id
def assign_participant(self, participant_id: int, slot: Literal[-1, 1, 2]) -> None: def assign_participant(self, participant_id: int, slot: Literal[1, 2]) -> None:
if slot == -1:
raise TournamentError("Normal match does not support slot -1")
new_participant = MatchParticipant(participant_id, slot) new_participant = MatchParticipant(participant_id, slot)
if len(self._participants) < 2 and not any(p.participant_id == participant_id for p in self._participants): if len(self._participants) < 2 and not any(p.participant_id == participant_id for p in self._participants):
if len(self._participants) == 1 and self._participants[-1].slot_number == new_participant.slot_number: if len(self._participants) == 1 and self._participants[-1].slot_number == new_participant.slot_number:
@ -133,28 +131,3 @@ class Match:
) )
return (f"<Match id={self._match_id} round={self._round_number} bracket={self._bracket.name} " return (f"<Match id={self._match_id} round={self._round_number} bracket={self._bracket.name} "
f"status={self.status.name} participants=[{participants}] winner={self.winner}>") f"status={self.status.name} participants=[{participants}] winner={self.winner}>")
class FFAMatch(Match):
"""
Specialized match that supports infinite participants
"""
def __init__(self, match_id: int, tournament_id: int, round_number: int, bracket: Bracket, best_of: int, status: MatchStatus,
next_match_win_lose_ids: tuple[Optional[int], Optional[int]], match_has_ended_callback: Callable) -> None:
super().__init__(match_id, tournament_id, round_number, bracket, best_of, status, next_match_win_lose_ids, match_has_ended_callback)
@property
def is_fully_seeded(self) -> bool:
return len(self._participants) > 1
def assign_participant(self, participant_id: int, slot: Literal[-1, 1, 2]) -> None:
if slot != -1:
raise TournamentError("FFAMatch does not support slot 1 and 2")
new_participant = MatchParticipant(participant_id, slot)
self._participants.append(new_participant)
def __repr__(self) -> str:
participants = ", ".join(
f"{p.participant_id}" for p in self._participants
)
return (f"<Match id={self._match_id} round={self._round_number} bracket={self._bracket.name} "
f"status={self.status.name} participants=[{participants}] winner={self.winner}>")

View File

@ -3,7 +3,7 @@ from datetime import datetime
from typing import Optional from typing import Optional
from math import ceil, log2 from math import ceil, log2
from src.ezgg_lan_manager.types.Match import Match, FFAMatch from src.ezgg_lan_manager.types.Match import Match
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 from src.ezgg_lan_manager.types.TournamentBase import GameTitle, TournamentFormat, TournamentStatus, TournamentError, Bracket, MatchStatus
@ -93,12 +93,8 @@ class Tournament:
def remove_participant(self, participant: Participant) -> None: def remove_participant(self, participant: Participant) -> None:
if participant.id not in (p.id for p in self._participants): if participant.id not in (p.id for p in self._participants):
raise TournamentError(f"Participant with ID {participant.id} not registered for tournament") raise TournamentError(f"Participant with ID {participant.id} not registered for tournament")
# ToDo: Check if tournament already started => correctly resolve matches with now missing participant
self._participants.remove(participant) self._participants.remove(participant)
def cancel(self):
self.status = TournamentStatus.CANCELED
def match_has_ended_callback(self, match: Match) -> None: def match_has_ended_callback(self, match: Match) -> None:
if self._matches is None: if self._matches is None:
return return
@ -132,12 +128,10 @@ class Tournament:
bracket = "SINGLE" bracket = "SINGLE"
elif fmt.name.startswith("DOUBLE_ELIMINATION"): elif fmt.name.startswith("DOUBLE_ELIMINATION"):
bracket = "DOUBLE" bracket = "DOUBLE"
elif fmt.name.startswith("FFA"):
bracket = "FINAL"
else: else:
raise TournamentError(f"Unsupported tournament format: {fmt}") raise TournamentError(f"Unsupported tournament format: {fmt}")
if fmt.name.endswith("_BO_1") or fmt.name.endswith("FFA"): if fmt.name.endswith("_BO_1"):
bo = 1 bo = 1
elif fmt.name.endswith("_BO_3"): elif fmt.name.endswith("_BO_3"):
bo = 3 bo = 3
@ -155,28 +149,7 @@ class Tournament:
num_participants = len(self.participants) num_participants = len(self.participants)
match_id_counter = 1 match_id_counter = 1
if bracket_type == "FINAL": if bracket_type == "SINGLE":
rounds: list[list[Match]] = []
round_matches = []
match = FFAMatch(
match_id=match_id_counter,
tournament_id=self._id,
round_number=1,
bracket=Bracket.FINAL,
best_of=best_of,
status=MatchStatus.WAITING,
next_match_win_lose_ids=(None, None),
match_has_ended_callback=self.match_has_ended_callback
)
for participant in self.participants:
match.assign_participant(participant.id, -1)
round_matches.append(match)
rounds.append(round_matches)
self._matches = [match]
elif bracket_type == "SINGLE":
# --- single-elimination as before --- # --- single-elimination as before ---
num_rounds = ceil(log2(num_participants)) num_rounds = ceil(log2(num_participants))
rounds: list[list[Match]] = [] rounds: list[list[Match]] = []

View File

@ -16,10 +16,9 @@ class TournamentFormat(Enum):
DOUBLE_ELIMINATION_BO_1 = 4 DOUBLE_ELIMINATION_BO_1 = 4
DOUBLE_ELIMINATION_BO_3 = 5 DOUBLE_ELIMINATION_BO_3 = 5
DOUBLE_ELIMINATION_BO_5 = 6 DOUBLE_ELIMINATION_BO_5 = 6
FFA = 7
def tournament_format_to_display_texts(tournament_format: TournamentFormat) -> tuple[str, str]: def tournament_format_to_display_texts(tournament_format: TournamentFormat) -> tuple[str, str]:
""" Returns tuple where idx 0 is SE/DE/FFA string and idx 1 is match count """ """ Returns tuple where idx 0 is SE/DE string and idx 1 is match count """
if tournament_format == TournamentFormat.SINGLE_ELIMINATION_BO_1: if tournament_format == TournamentFormat.SINGLE_ELIMINATION_BO_1:
return "Single Elimination", "1" return "Single Elimination", "1"
elif tournament_format == TournamentFormat.SINGLE_ELIMINATION_BO_3: elif tournament_format == TournamentFormat.SINGLE_ELIMINATION_BO_3:
@ -32,8 +31,6 @@ def tournament_format_to_display_texts(tournament_format: TournamentFormat) -> t
return "Double Elimination", "3" return "Double Elimination", "3"
elif tournament_format == TournamentFormat.DOUBLE_ELIMINATION_BO_5: elif tournament_format == TournamentFormat.DOUBLE_ELIMINATION_BO_5:
return "Double Elimination", "5" return "Double Elimination", "5"
elif tournament_format == TournamentFormat.FFA:
return "Free for All", "1"
else: else:
raise RuntimeError(f"Unknown tournament status: {str(tournament_format)}") raise RuntimeError(f"Unknown tournament status: {str(tournament_format)}")

View File

@ -64,19 +64,4 @@ class TournamentDomainTests(unittest.TestCase):
self.assertEqual(sm.status, MatchStatus.WAITING) self.assertEqual(sm.status, MatchStatus.WAITING)
self.assertEqual(sm.participants[0].participant_id, self.participant_c.id) self.assertEqual(sm.participants[0].participant_id, self.participant_c.id)
self.assertEqual(sm.participants[0].slot_number, 1) self.assertEqual(sm.participants[0].slot_number, 1)
self.assertIsNone(sm.winner) self.assertIsNone(sm.winner)
def test_ffa_tournament_with_15_participants_gets_generated_correctly(self) -> None:
tournament_under_test = generate_new_tournament("Among Us", "It's Among Us", GameTitle("Among Us", "", "", ""), TournamentFormat.FFA, self.start_time, 32, TournamentStatus.OPEN)
for i in range(1, 16):
tournament_under_test.add_participant(Participant(i, ParticipantType.PLAYER))
tournament_under_test.start()
# Assert Tournament was switched to ONGOING
self.assertEqual(TournamentStatus.ONGOING, tournament_under_test.status)
matches_in_tournament = sorted(tournament_under_test.matches, key=lambda m: m.match_id)
self.assertEqual(1, len(matches_in_tournament))
self.assertEqual(15, len(matches_in_tournament[0].participants))