aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaran Jayachandra <mail@karanjayachandra.com>2024-12-02 19:50:15 +0000
committerKaran Jayachandra <mail@karanjayachandra.com>2024-12-02 19:50:15 +0000
commitfd03b6dd91be67b96356827a2ce4b7728d2e25ea (patch)
tree8d9cf47a6bbac47fe9eeffbc0d92a1eef4323572
parent7b7f8cdef5f035ccdab714059c1a6c54b80c4ba3 (diff)
parent662e0854a6c61503954aa659e1c932db69443a06 (diff)
Merge branch 'develop' into 'main'
Develop See merge request KaranJayachandra/match_up!12
-rw-r--r--src/match_up/controller.py56
-rw-r--r--src/match_up/data.py8
-rw-r--r--src/match_up/model.py100
-rw-r--r--src/match_up/routes.py33
-rw-r--r--src/match_up/templates/controls.j212
-rw-r--r--src/match_up/templates/players.j214
-rw-r--r--src/match_up/utilities.py117
7 files changed, 186 insertions, 154 deletions
diff --git a/src/match_up/controller.py b/src/match_up/controller.py
index 0e08cae..b00bf6b 100644
--- a/src/match_up/controller.py
+++ b/src/match_up/controller.py
@@ -2,7 +2,14 @@ from typing import Tuple
from operator import attrgetter
from match_up.data import Game, PLAYER_PER_COURT
from match_up.model import Player, PlayerList, CourtList
-from match_up.utilities import _create_placeholders, _select_players, _create_games
+from match_up.utilities import (
+ _create_placeholders,
+ _select_players,
+ _create_shuffle_games,
+ _create_team_games,
+ separate_teams_and_players,
+ split_courts,
+)
class RoundGenerator:
@@ -30,21 +37,31 @@ class RoundGenerator:
return games
def _create_active_games(
- self, total_levels: int
+ self, total_levels: int, group_by_team: bool
) -> Tuple[list[Game], list[Player]]:
- active_courts, _ = self.courts.separate_courts()
- used_courts, _ = self._separate_courts(active_courts)
+ active_courts = self.courts.separate_courts()[0]
+ used_courts = self._separate_courts(active_courts)[0]
required_player_count = len(used_courts) * PLAYER_PER_COURT
active_players = self.players.get_players()
- selected_players = _select_players(active_players, required_player_count)
- games = _create_games(used_courts, selected_players, total_levels)
- return games, selected_players
-
- def _generate_proposal(self, total_levels: int) -> Tuple[list[Game], list[Player]]:
- inactive_games = self._create_inactive_games()
- active_games, selected_players = self._create_active_games(total_levels)
- games = active_games + inactive_games
- games.sort(key=attrgetter("court"))
+ if group_by_team:
+ active_teams, team_less_players = separate_teams_and_players(active_players)
+ regular_court_count = len(used_courts) - len(active_teams)
+ regular_courts, team_courts = split_courts(used_courts, regular_court_count)
+ new_player_count = required_player_count - (
+ PLAYER_PER_COURT * len(active_teams)
+ )
+ regular_players = _select_players(team_less_players, new_player_count)
+ regular_games = _create_shuffle_games(
+ regular_courts, regular_players, total_levels
+ )
+ team_games, selected_team_players = _create_team_games(
+ team_courts, active_teams
+ )
+ selected_players = regular_players + selected_team_players
+ games = regular_games + team_games
+ else:
+ selected_players = _select_players(active_players, required_player_count)
+ games = _create_shuffle_games(used_courts, selected_players, total_levels)
return games, selected_players
def update_player_list(self, csv_location):
@@ -55,10 +72,15 @@ class RoundGenerator:
self.proposed_players = []
return self.games
- def propose(self, levels: int) -> list[Game]:
- proposal = self._generate_proposal(levels)
- self.proposed_games = proposal[0]
- self.proposed_players = proposal[1]
+ def propose(self, levels: int, group_by_team: bool) -> list[Game]:
+ inactive_games = self._create_inactive_games()
+ active_games, selected_players = self._create_active_games(
+ levels, group_by_team
+ )
+ games = active_games + inactive_games
+ games.sort(key=attrgetter("court"))
+ self.proposed_games = games
+ self.proposed_players = selected_players
return self.proposed_games
def confirm(self) -> list[Game]:
diff --git a/src/match_up/data.py b/src/match_up/data.py
index 62aa5d8..c00dde2 100644
--- a/src/match_up/data.py
+++ b/src/match_up/data.py
@@ -6,6 +6,8 @@ from sqlalchemy.ext.declarative import declarative_base
MIN_LEVEL = 1
MAX_LEVEL = 10
PLAYER_PER_COURT = 4
+GUEST_LEVELS = [1, 3, 5]
+GUESTS_PER_LEVEL = 4
BASE = declarative_base()
@@ -16,6 +18,7 @@ class Player(BASE):
first = Column("first", String)
last = Column("last", String)
level = Column("level", Integer)
+ team = Column("team", Integer)
status = Column("status", Integer)
count = Column("count", Integer)
@@ -25,6 +28,7 @@ class Player(BASE):
first: str = "",
last: str = "",
level: int = 0,
+ team: int = 0,
status: bool = True,
count: int = 0,
):
@@ -32,12 +36,14 @@ class Player(BASE):
self.first = first
self.last = last
self.status = status
+ self.team = team
self.level = level
self.count = count
def __repr__(self):
+ team = f" of Team {self.team}" if self.team else ""
status = "active" if self.status else "inactive"
- return f"{self.first} {self.last} (Level {self.level}) is {status} and has played {self.count} games"
+ return f"{self.first} {self.last} (Level {self.level}){team} is {status} and has played {self.count} games"
class DisplayPlayer:
diff --git a/src/match_up/model.py b/src/match_up/model.py
index c42823b..467310b 100644
--- a/src/match_up/model.py
+++ b/src/match_up/model.py
@@ -1,21 +1,22 @@
from math import floor
from typing import Tuple
from os.path import isfile
+from pandas import read_csv
from dataclasses import dataclass
from importlib.resources import path
+from random import getrandbits, randint
from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine, not_
-from match_up.utilities import (
- generate_guests,
- _init_database_from_csv,
- _init_random_database,
- _textualize_guest,
-)
+from names import get_first_name, get_last_name
from match_up.data import (
- Player,
- DisplayPlayer,
+ MIN_LEVEL,
+ MAX_LEVEL,
PLAYER_PER_COURT,
BASE,
+ GUEST_LEVELS,
+ GUESTS_PER_LEVEL,
+ Player,
+ DisplayPlayer,
)
@@ -43,8 +44,63 @@ class CourtList:
return active_courts, inactive_courts
+def _init_guests(session) -> None:
+ for level_id, level in enumerate(GUEST_LEVELS):
+ for player_id in range(GUESTS_PER_LEVEL):
+ unique_id = (level_id * GUESTS_PER_LEVEL) + player_id + 1
+ player = Player(
+ id=-unique_id,
+ first="Guest",
+ last=str(unique_id),
+ level=level,
+ status=False,
+ count=0,
+ )
+ session.add(player)
+ session.commit()
+
+
+def _init_database_from_csv(session, csv_location: str) -> None:
+ data = read_csv(csv_location)
+ session.query(Player).delete()
+ for _, entry in data.iterrows():
+ player = Player(
+ id=entry["id"],
+ first=entry["first"],
+ last=entry["last"],
+ level=entry["skill"],
+ team=entry["team"],
+ status=False,
+ count=0,
+ )
+ session.add(player)
+ session.commit()
+ _init_guests(session)
+
+
+def _init_random_database(session, player_count: int = 80) -> None:
+ total_teams = 5
+ players_per_team = 5
+ for i in range(player_count):
+ if i < total_teams * players_per_team:
+ team = floor(i / players_per_team) + 1
+ else:
+ team = 0
+ player = Player(
+ id=i,
+ first=get_first_name(),
+ last=get_last_name(),
+ level=randint(MIN_LEVEL, MAX_LEVEL),
+ team=team,
+ status=bool(getrandbits(1)),
+ count=0,
+ )
+ session.add(player)
+ session.commit()
+ _init_guests(session)
+
+
class PlayerList:
- guest_count: dict = {1: 0, 3: 0, 5: 0}
def __init__(self, csv_location=None) -> None:
with path("match_up", "players.db") as p:
@@ -64,16 +120,6 @@ class PlayerList:
BASE.metadata.create_all(bind=engine)
self.session = sessionmaker(bind=engine)()
- def increment_guests(self, level: int):
- self.guest_count[level] = self.guest_count[level] + 1
- return _textualize_guest(level, self.guest_count[level])
-
- def decrement_guests(self, level: int):
- if self.guest_count[level] == 0:
- return 0
- self.guest_count[level] = self.guest_count[level] - 1
- return _textualize_guest(level, self.guest_count[level])
-
def get_player_status(self, id: int) -> bool:
return self.session.query(Player).filter(Player.id == id).first().status
@@ -83,14 +129,17 @@ class PlayerList:
self.session.commit()
return DisplayPlayer(player)
- def get_all_players(self) -> list[DisplayPlayer]:
+ def get_all_players(self) -> Tuple[list[DisplayPlayer], list[DisplayPlayer]]:
all_players = [
DisplayPlayer(p)
for p in self.session.query(Player)
.order_by(Player.first, Player.last)
.all()
]
- return all_players
+ regulars = [player for player in all_players if player.first != "Guest"]
+ guests = [player for player in all_players if player.first == "Guest"]
+ guests.sort(key=lambda x: int(x.last))
+ return regulars, guests
def get_possible_game_count(self) -> int:
total_players = len(self.get_players())
@@ -104,14 +153,7 @@ class PlayerList:
.filter(not_(Player.status == 0))
.all()
)
- guests = generate_guests(self.guest_count)
- return active_players + guests
-
- def get_guests(self):
- return {
- level: _textualize_guest(level, self.guest_count[level])
- for level in self.guest_count
- }
+ return active_players
def increment_game_count(self, player_ids: list[int]):
players = self.session.query(Player).filter(Player.id.in_(player_ids))
diff --git a/src/match_up/routes.py b/src/match_up/routes.py
index c0e77aa..e5a3b99 100644
--- a/src/match_up/routes.py
+++ b/src/match_up/routes.py
@@ -15,31 +15,13 @@ def get_controls():
return render_template("controls.j2", courts=_rg.courts.get_courts())
-@_app.route("/increment_guest", methods=["POST"])
-def increment_guests():
- level = int(request.form["level"].split()[0])
- t = re.from_string(
- '{% from "macros.j2" import guest_control %}{{guest_control(level, value)}}'
- )
- value = _rg.players.increment_guests(level)
- return render_template(t, level=level, value=value)
-
-
-@_app.route("/decrement_guest", methods=["POST"])
-def decrement_guests():
- print(request.form["level"])
- level = int(request.form["level"])
- t = re.from_string(
- '{% from "macros.j2" import guest_control %}{{guest_control(level, value)}}'
- )
- value = _rg.players.decrement_guests(level)
- return render_template(t, level=level, value=value)
-
-
@_app.route("/propose", methods=["POST"])
def get_random_games():
- levels = int(request.form["type"].split()[0])
- return render_template("games.j2", games=_rg.propose(levels), title="Proposal")
+ levels = int(request.form["levels"].split()[0])
+ team_practice = request.form["team"] == "Teams"
+ return render_template(
+ "games.j2", games=_rg.propose(levels, team_practice), title="Proposal"
+ )
@_app.route("/confirm", methods=["POST"])
@@ -69,10 +51,9 @@ def toggle_player(player_request):
@_app.route("/players", methods=["GET"])
def get_list_of_players():
- all_players = _rg.players.get_all_players()
- guests = _rg.players.get_guests()
+ regulars, guests = _rg.players.get_all_players()
return re.get_template("players.j2").render(
- players=all_players, guests=guests, url_for=url_for
+ regulars=regulars, guests=guests, url_for=url_for
)
diff --git a/src/match_up/templates/controls.j2 b/src/match_up/templates/controls.j2
index 24b42c6..0183c97 100644
--- a/src/match_up/templates/controls.j2
+++ b/src/match_up/templates/controls.j2
@@ -9,7 +9,7 @@
<div class="columns">
<div class="column">
<div class="select is-responsive">
- <select name="type">
+ <select name="levels">
<option>10 Levels</option>
<option>8 Levels</option>
<option>6 Levels</option>
@@ -20,10 +20,18 @@
</div>
</div>
<div class="column">
+ <div class="select is-responsive">
+ <select name="team">
+ <option>Random</option>
+ <option>Teams</option>
+ </select>
+ </div>
+ </div>
+ <div class="column">
<button class="button is-dark is-responsive"
hx-post="/propose"
hx-target="#games"
- hx-include="[name='type']">Create</button>
+ hx-include="[name='levels'], [name='team']">Generate</button>
</div>
<div class="column"></div>
<div class="column">
diff --git a/src/match_up/templates/players.j2 b/src/match_up/templates/players.j2
index cd41925..0beeee9 100644
--- a/src/match_up/templates/players.j2
+++ b/src/match_up/templates/players.j2
@@ -5,19 +5,19 @@
<body>
<section class="section">
{{ navbar(url_for) }}
- <h4 class="title is-4">Players</h4>
+ <h4 class="title is-4">Members</h4>
<div class="fixed-grid has-4-cols">
<div class="grid">
- {% for player in players %}
- <div class="cell"> {{ player_button(player) }} </div>
+ {% for regular in regulars %}
+ <div class="cell"> {{ player_button(regular) }} </div>
{% endfor %}
</div>
<h4 class="title is-4">Guests</h4>
- <div class="fixed-grid has-3-cols">
+ <div class="fixed-grid has-4-cols">
<div class="grid">
- {% for level in guests %}
- <div class="cell"> {{ guest_control(level, guests[level]) }} </div>
- {% endfor %}
+ {% for guest in guests %}
+ <div class="cell"> {{ player_button(guest) }} </div>
+ {% endfor %}
</div>
</section>
<section class="section">
diff --git a/src/match_up/utilities.py b/src/match_up/utilities.py
index db7c76a..7140a5f 100644
--- a/src/match_up/utilities.py
+++ b/src/match_up/utilities.py
@@ -1,17 +1,7 @@
from typing import Tuple
-from pandas import read_csv
-from random import shuffle, choice
-from random import randint, getrandbits
-from names import get_first_name, get_last_name
-from match_up.data import (
- Player,
- DisplayPlayer,
- Team,
- Game,
- MIN_LEVEL,
- MAX_LEVEL,
- PLAYER_PER_COURT,
-)
+from random import shuffle, sample
+from match_up.data import Player, DisplayPlayer, Team, Game
+from match_up.data import MAX_LEVEL, PLAYER_PER_COURT
def _shuffle_two_lists(a: list, b: list) -> Tuple[list, list]:
@@ -71,7 +61,9 @@ def _create_groups_of_four_players(
return court_players
-def _create_games(courts: dict, players: list[Player], levels: int) -> list[Game]:
+def _create_shuffle_games(
+ courts: dict, players: list[Player], levels: int
+) -> list[Game]:
court_players = _create_groups_of_four_players(players, levels)
games = []
for court, players in zip(courts, court_players):
@@ -85,66 +77,47 @@ def _create_games(courts: dict, players: list[Player], levels: int) -> list[Game
return games
-def generate_guest_type(count: int, level: int, description: str):
- base_id = level * 10000
- return [
- Player(
- id=base_id + i,
- first=description,
- last=f"{i + 1}",
- level=level,
- status=True,
- count=choice([0, 1000]),
+def _create_team_games(courts: dict, teams: dict) -> tuple[list[Game], list[Player]]:
+ games = []
+ selected_players = []
+ for court, (_, players) in zip(courts, teams.items()):
+ players = sample(players, PLAYER_PER_COURT)
+ selected_players += players
+ players.sort(key=lambda x: x.level, reverse=True)
+ team1 = Team(
+ player1=DisplayPlayer(players[0]), player2=DisplayPlayer(players[3])
)
- for i in range(count)
- ]
-
-
-def generate_guests(guests):
- beginner_guests = generate_guest_type(guests[1], 1, "Beginner")
- novice_guests = generate_guest_type(guests[3], 3, "Novice")
- intermediate_guests = generate_guest_type(guests[5], 3, "Intermediate")
- return beginner_guests + novice_guests + intermediate_guests
-
-
-def _textualize_guest(level, value) -> str:
- match level:
- case 1:
- text = "Beginners"
- case 3:
- text = "Novices"
- case 5:
- text = "Intermediates"
- if value == 1:
- text = text[:-1]
- return f"{value} {text}"
+ team2 = Team(
+ player1=DisplayPlayer(players[1]), player2=DisplayPlayer(players[2])
+ )
+ games.append(Game(court, team1, team2))
+ return games, selected_players
-def _init_database_from_csv(session, csv_location: str) -> None:
- data = read_csv(csv_location)
- session.query(Player).delete()
- for _, entry in data.iterrows():
- player = Player(
- id=entry["id"],
- first=entry["first"],
- last=entry["last"],
- level=entry["skill"],
- status=False,
- count=0,
- )
- session.add(player)
- session.commit()
+def separate_teams_and_players(players: list[Player]) -> Tuple[dict, list[Player]]:
+ unique_team_id = {player.team for player in players}
+ grouped_players_by_team = {team_id: [] for team_id in unique_team_id}
+ for player in players:
+ team = player.team
+ grouped_players_by_team[team].append(player)
+ active_teams = {}
+ players_without_team = []
+ for team_id in grouped_players_by_team:
+ if team_id == 0:
+ players_without_team += [
+ player for player in grouped_players_by_team[team_id]
+ ]
+ else:
+ if len(grouped_players_by_team[team_id]) >= 4:
+ active_teams[team_id] = grouped_players_by_team[team_id]
+ else:
+ players_without_team += [
+ player for player in grouped_players_by_team[team_id]
+ ]
+ return active_teams, players_without_team
-def _init_random_database(session, player_count: int = 80) -> None:
- for i in range(player_count):
- player = Player(
- id=i,
- first=get_first_name(),
- last=get_last_name(),
- level=randint(MIN_LEVEL, MAX_LEVEL),
- status=bool(getrandbits(1)),
- count=0,
- )
- session.add(player)
- session.commit()
+def split_courts(courts: dict, n) -> Tuple[dict, dict]:
+ first_half = {k: v for (k, v) in [x for x in courts.items()][:n]}
+ second_half = {k: v for (k, v) in [x for x in courts.items()][n:]}
+ return first_half, second_half