diff options
Diffstat (limited to 'src/match_up')
| -rw-r--r-- | src/match_up/__init__.py | 2 | ||||
| -rw-r--r-- | src/match_up/controller.py | 83 | ||||
| -rw-r--r-- | src/match_up/model.py | 224 | ||||
| -rw-r--r-- | src/match_up/routes.py | 69 | ||||
| -rw-r--r-- | src/match_up/templates/controls.j2 | 28 | ||||
| -rw-r--r-- | src/match_up/templates/games.j2 | 10 | ||||
| -rw-r--r-- | src/match_up/templates/macros.j2 | 14 | ||||
| -rw-r--r-- | src/match_up/templates/players.j2 | 63 | ||||
| -rw-r--r-- | src/match_up/tests/test_db.py | 15 |
9 files changed, 348 insertions, 160 deletions
diff --git a/src/match_up/__init__.py b/src/match_up/__init__.py index 3aa3b98..61a8416 100644 --- a/src/match_up/__init__.py +++ b/src/match_up/__init__.py @@ -27,5 +27,5 @@ def main(): global rg rg.courts = CourtList(total=args.courts) rg.timer = Timer(max_mins=args.timer) - rg.players = PlayerList(location=args.database) + rg.players = PlayerList(csv_location=args.database) app.run() diff --git a/src/match_up/controller.py b/src/match_up/controller.py index a21fced..3b4a6f9 100644 --- a/src/match_up/controller.py +++ b/src/match_up/controller.py @@ -5,6 +5,8 @@ from match_up.utilities import sample_list, Timer from match_up.model import ( Team, Game, + Player, + DisplayPlayer, PlayerList, CourtList, MAX_LEVEL, @@ -13,37 +15,51 @@ from match_up.model import ( def _create_placeholders(courts: dict, name: str) -> list[Game]: - dummy_team = Team(player1=name, player2=name) + dummy_player = Player(first=name) + dummy_team = Team( + player1=DisplayPlayer(dummy_player), player2=DisplayPlayer(dummy_player) + ) games = [Game(court, dummy_team, dummy_team) for court in courts] return games -def _normalize_skill(players: dict, new_max: int) -> int: - player_levels = [] - for _, player in players.items(): - skill_level = player.skill - new_level = round((new_max - 1) * (skill_level / MAX_LEVEL)) + 1 - player_levels.append(new_level) - return player_levels +def _normalize_skill(level: int, new_max: int) -> int: + new_level = round((new_max - 1) * (level / MAX_LEVEL)) + 1 + return new_level -def _create_groups_of_four_players(players: dict, new_max: int) -> list[dict]: - player_levels = _normalize_skill(players, new_max) - identifiers = sample_list([*players], player_levels) - clumps = [] +def _select_players(players: list[Player], count: int) -> dict: + identifiers = [player.id for player in players] + play_count = [player.count for player in players] + ids = sample_list(identifiers, play_count, count) + selected_players = [player for player in players if player.id in ids] + return selected_players + + +def _create_groups_of_four_players( + players: list[Player], new_max: int +) -> list[list[Player]]: + identifiers = [player.id for player in players] + player_levels = [_normalize_skill(player.level, new_max) for player in players] + identifiers = sample_list(identifiers, player_levels) + court_players = [] for i in range(0, len(players), PLAYER_PER_COURT): - clumps.append(identifiers[i : i + PLAYER_PER_COURT]) - combinations = [{id: players[id] for id in clump} for clump in clumps] - return combinations + court_ids = identifiers[i : i + PLAYER_PER_COURT] + selected_players = [player for player in players if player.id in court_ids] + court_players.append(selected_players) + return court_players -def _create_games(courts: dict, players: list[dict], levels: int): - grouped_players = _create_groups_of_four_players(players, levels) +def _create_games(courts: dict, players: list[Player], levels: int): + court_players = _create_groups_of_four_players(players, levels) games = [] - for court, players in zip(courts, grouped_players): - ids = [*players] - team1 = Team(player1=players[ids[0]].name, player2=players[ids[3]].name) - team2 = Team(player1=players[ids[1]].name, player2=players[ids[2]].name) + for court, players in zip(courts, court_players): + team1 = Team( + player1=DisplayPlayer(players[0]), player2=DisplayPlayer(players[3]) + ) + team2 = Team( + player1=DisplayPlayer(players[1]), player2=DisplayPlayer(players[2]) + ) games.append(Game(court, team1, team2)) return games @@ -57,8 +73,7 @@ class RoundGenerator: def __post_init__(self) -> None: self.timer = Timer() - self.history = {key: 0 for key, _ in self.players.get_players().items()} - self.games = self._generate_proposal(MAX_LEVEL)[0] + self.games = _create_placeholders(self.courts.get_courts(), "---") def _separate_courts(self, courts: dict) -> Tuple[dict, dict]: possible_games = min(len(courts), self.players.get_possible_game_count()) @@ -67,20 +82,6 @@ class RoundGenerator: unused_courts = dict(courts_as_list[possible_games:]) return used_courts, unused_courts - def _select_players(self, count: int) -> dict: - active_players = self.players.get_potential_players() - identifiers = [*active_players] - play_count = [ - ( - self.history[player_id] - if player_id < self.players.guest_id_start - else self.round - ) - for player_id in active_players - ] - ids = sample_list(identifiers, play_count, count) - return {player_id: active_players[player_id] for player_id in ids} - def _create_inactive_games(self) -> list[Game]: active_courts, reserved_courts = self.courts.separate_courts() reserved_games = _create_placeholders(reserved_courts, "RESERVED") @@ -93,7 +94,8 @@ class RoundGenerator: active_courts, _ = self.courts.separate_courts() used_courts, _ = self._separate_courts(active_courts) required_player_count = len(used_courts) * PLAYER_PER_COURT - selected_players = self._select_players(required_player_count) + 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 @@ -117,9 +119,8 @@ class RoundGenerator: if self.games == proposed_games or len(proposed_games) == 0: return self.games self.games = proposed_games - for player_id in proposed_players: - if player_id < self.players.guest_id_start: - self.history[player_id] = self.history[player_id] + 1 + identifiers = [player.id for player in proposed_players] + self.players.increment_game_count(identifiers) self.proposal = tuple() self.round += 1 self.timer.start() diff --git a/src/match_up/model.py b/src/match_up/model.py index 5d6c0f5..4f8ec8c 100644 --- a/src/match_up/model.py +++ b/src/match_up/model.py @@ -1,9 +1,15 @@ from math import floor -from csv import reader from typing import Tuple -from names import get_full_name +from os.path import join, isfile from dataclasses import dataclass +from importlib.resources import path +from pandas import read_csv, DataFrame +from sqlalchemy.orm import sessionmaker from random import randint, getrandbits +from names import get_first_name, get_last_name +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import create_engine, Column, String, Integer, not_ + MIN_LEVEL = 1 MAX_LEVEL = 10 @@ -11,17 +17,60 @@ DEFAULT_COURTS = 12 PLAYER_PER_COURT = 4 -@dataclass -class Player: - name: str - skill: int = 1 - status: bool = False +Base = declarative_base() + + +class Player(Base): + __tablename__ = "players" + id = Column("id", Integer, primary_key=True) + first = Column("first", String) + last = Column("last", String) + level = Column("level", Integer) + status = Column("status", Integer) + count = Column("count", Integer) + + def __init__( + self, + id: int = None, + first: str = "", + last: str = "", + level: int = None, + status: bool = None, + count: int = None, + ): + self.id = id + self.first = first + self.last = last + self.status = status + self.level = level + self.count = count + + def __repr__(self): + status = "active" if self.status else "inactive" + return f"{self.first} {self.last} (Level {self.level}) is {status} and has played {self.count} games" + + +class DisplayPlayer: + id: int + first: str + last: str + status: bool + + def __init__(self, player: Player): + self.id = player.id + self.first = player.first + self.last = player.last + self.status = player.status + + def __repr__(self): + status = "active" if self.status else "inactive" + return f"{self.first} {self.last} is {status}" @dataclass class Team: - player1: str - player2: str + player1: DisplayPlayer + player2: DisplayPlayer @dataclass @@ -55,69 +104,120 @@ class CourtList: return active_courts, inactive_courts -def _load_csv_players(file_name: str) -> dict: - with open(file_name, newline="") as file: - pointer = reader(file) - next(pointer, None) - players = {id: Player(row[0], int(row[1])) for id, row in enumerate(pointer)} - return players - - -def _generate_random_players(player_count: int = 80) -> list[Player]: - players = { - i: Player( - name=get_full_name(), - skill=randint(MIN_LEVEL, MAX_LEVEL), - status=bool(getrandbits(1)), - ) - for i in range(player_count) - } - return players - - -@dataclass class PlayerList: - location: str = None - guests: int = 0 + guests: dict = {"low": 0, "mid": 0, "high": 0} - def __post_init__(self) -> None: - if self.location is not None: - self.players = _load_csv_players(self.location) - else: - self.players = _generate_random_players() - self.guest_id_start = len(self.players) + 1 + def __init__(self, csv_location: str = None) -> None: + with path("match_up") as p: + self.db_location = join(p, "players.db") + generate_random_players = False + if not isfile(self.db_location): + print(f"No database file found") + if csv_location is None: + generate_random_players = True + engine = create_engine("sqlite:///" + self.db_location, echo=True) + Base.metadata.create_all(bind=engine) + self.session = sessionmaker(bind=engine)() + if csv_location is not None: + print(f"CSV file provided, updating the database") + self.session.query(Player).delete() + self._init_database_from_csv(read_csv(self.csv_location)) + elif generate_random_players: + print(f"Generating random players for testing") + self._init_random_database() - def increment_guests(self): - self.guests = self.guests + 1 - return self.guests + def increment_guests(self, level: str): + self.guests[level] = self.guests[level] + 1 + return self.guests[level] - def decrement_guests(self): - if self.guests == 0: + def decrement_guests(self, level: str): + if self.guests[level] == 0: return 0 - self.guests = self.guests - 1 - return self.guests + self.guests[level] = self.guests[level] - 1 + return self.guests[level] - def get_player_status(self, player_number: int) -> bool: - return self.players[player_number].status + def get_player_status(self, id: int) -> bool: + return self.session.query(Player).filter(Player.id == id).first().status - def toggle_player_status(self, player_number: int) -> Tuple[bool, str]: - new_status = not self.players[player_number].status - self.players[player_number].status = new_status - return new_status, self.players[player_number].name + def toggle_player_status(self, id: int) -> DisplayPlayer: + player = self.session.query(Player).filter(Player.id == id).first() + player.status = not player.status + self.session.commit() + return DisplayPlayer(player) - def get_players(self) -> dict: - return self.players + def get_all_players(self) -> list[Player]: + all_players = [DisplayPlayer(p) for p in self.session.query(Player).all()] + return all_players def get_possible_game_count(self) -> int: - total_players = len(self.get_potential_players()) + total_players = len(self.get_players()) possible_games = floor(total_players / PLAYER_PER_COURT) return possible_games - def get_potential_players(self) -> dict: - players = {k: v for k, v in self.players.items() if v.status} - guests = { - self.guest_id_start + k: Player(name=f"Guest {k}") - for k in range(1, self.guests + 1) - } - players.update(guests) - return players + def get_players(self, rounds: int = 0) -> list[Player]: + active_players = ( + self.session.query(Player).filter(not_(Player.status == 0)).all() + ) + low_guests = [ + Player( + id=1000 + i, + first=f"Beginner", + last=f"{i}", + level=1, + status=True, + count=rounds, + ) + for i in range(1, self.guests["low"] + 1) + ] + mid_guests = [ + Player( + id=2000 + i, + first=f"Novice", + last=f"{i}", + level=4, + status=True, + count=rounds, + ) + for i in range(1, self.guests["mid"] + 1) + ] + high_guests = [ + Player( + id=3000 + i, + first=f"Intermediate", + last=f"{i}", + level=7, + status=True, + count=rounds, + ) + for i in range(1, self.guests["high"] + 1) + ] + return active_players + low_guests + mid_guests + high_guests + + def increment_game_count(self, player_ids: list[int]): + return 0 + + def _init_database_from_csv(self, data: DataFrame) -> None: + for _, entry in data.iterrows(): + player = Player( + id=entry["id"], + first=entry["first"], + last=entry["last"], + level=entry["skill"], + status=False, + count=0, + ) + self.session.add(player) + self.session.commit() + + def _init_random_database(self, 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=getrandbits(1), + count=0, + ) + self.session.add(player) + self.session.commit() diff --git a/src/match_up/routes.py b/src/match_up/routes.py index 319b111..95abe9e 100644 --- a/src/match_up/routes.py +++ b/src/match_up/routes.py @@ -22,22 +22,58 @@ def get_controls(): return render_template("controls.j2", courts=rg.courts.get_courts()) -@app.route("/guests", methods=["GET"]) -def get_guests(): +@app.route("/low_guests", methods=["GET"]) +def get_low_guests(): + t = re.from_string('{% from "macros.j2" import guests %}{{guests(count, level)}}') + return render_template(t, count=rg.players.guests["low"], level="low") + + +@app.route("/mid_guests", methods=["GET"]) +def get_mid_guests(): + t = re.from_string('{% from "macros.j2" import guests %}{{guests(count, level)}}') + return render_template(t, count=rg.players.guests["mid"], level="mid") + + +@app.route("/high_guests", methods=["GET"]) +def get_high_guests(): + t = re.from_string('{% from "macros.j2" import guests %}{{guests(count, level)}}') + return render_template(t, count=rg.players.guests["high"], level="high") + + +@app.route("/increment_low_guest", methods=["POST"]) +def increment_low_guests(): + t = re.from_string('{% from "macros.j2" import guests %}{{guests(count)}}') + return render_template(t, count=rg.players.increment_guests("low")) + + +@app.route("/decrement_low_guest", methods=["POST"]) +def decrement_low_guests(): t = re.from_string('{% from "macros.j2" import guests %}{{guests(count)}}') - return render_template(t, count=rg.players.guests) + return render_template(t, count=rg.players.decrement_guests("low")) -@app.route("/decrement", methods=["POST"]) -def increment_guests(): +@app.route("/increment_mid_guest", methods=["POST"]) +def increment_mid_guests(): t = re.from_string('{% from "macros.j2" import guests %}{{guests(count)}}') - return render_template(t, count=rg.players.decrement_guests()) + return render_template(t, count=rg.players.increment_guests("mid")) -@app.route("/increment", methods=["POST"]) -def decrement_guests(): +@app.route("/decrement_mid_guest", methods=["POST"]) +def decrement_mid_guests(): t = re.from_string('{% from "macros.j2" import guests %}{{guests(count)}}') - return render_template(t, count=rg.players.increment_guests()) + return render_template(t, count=rg.players.decrement_guests("mid")) + + +@app.route("/increment_high_guest", methods=["POST"]) +def increment_high_guests(): + t = re.from_string('{% from "macros.j2" import guests %}{{guests(count)}}') + return render_template(t, count=rg.players.increment_guests("high")) + + +@app.route("/decrement_high_guest", methods=["POST"]) +def decrement_high_guests(): + t = re.from_string('{% from "macros.j2" import guests %}{{guests(count)}}') + return render_template(t, count=rg.players.decrement_guests("high")) @app.route("/propose", methods=["POST"]) @@ -59,25 +95,26 @@ def clear_games(): @app.route("/player-toggle/<player_request>", methods=["POST"]) def toggle_player(player_request): id = int(player_request) - status, name = rg.players.toggle_player_status(id) + player = rg.players.toggle_player_status(id) t = re.from_string( - '{% from "macros.j2" import player %}{{player(id, status, name)}}' + '{% from "macros.j2" import player_button %}{{player_button(player)}}' ) - return render_template(t, id=id, status=status, name=name) + return render_template(t, player=player) @app.route("/players", methods=["GET"]) def get_list_of_players(): - return re.get_template("players.j2").render( - players=rg.players.get_players(), url_for=url_for - ) + all_players = rg.players.get_all_players() + return re.get_template("players.j2").render(players=all_players, url_for=url_for) @app.route("/court-toggle/<court_number>", methods=["POST"]) def toggle_court(court_number): id = int(court_number) status = rg.courts.toggle_court_status(id) - t = re.from_string('{% from "macros.j2" import court %}{{ court(id, status) }}') + t = re.from_string( + '{% from "macros.j2" import court_button %}{{ court_button(id, status) }}' + ) return render_template(t, id=id, status=status) diff --git a/src/match_up/templates/controls.j2 b/src/match_up/templates/controls.j2 index 6ba724a..e9e37f3 100644 --- a/src/match_up/templates/controls.j2 +++ b/src/match_up/templates/controls.j2 @@ -1,30 +1,13 @@ -{% from "macros.j2" import court %} +{% from "macros.j2" import court_button %} <div class="fixed-grid has-6-cols"> <div class="grid"> {% for key, value in courts.items() %} - {{ court(key, value) }} + {{ court_button(key, value) }} {% endfor %} </div> </div> <div class="columns"> <div class="column"> - <div class="field has-addons"> - <div class="control"> - <button class="button is-responsive is-dark" - hx-post="/decrement" - hx-swap="outerHTML" - hx-target="#guests">-</button> - </div> - <div hx-get="/guests" hx-swap="outerHTML" hx-trigger="revealed"></div> - <div class="control"> - <button class="button is-responsive is-dark" - hx-post="/increment" - hx-swap="outerHTML" - hx-target="#guests">+</button> - </div> - </div> - </div> - <div class="column"> <div class="select is-responsive"> <select name="type"> <option>10 Levels</option> @@ -40,7 +23,7 @@ <button class="button is-dark is-responsive" hx-post="/propose" hx-target="#games" - hx-include="[name='type']">New Round</button> + hx-include="[name='type']">Shuffle</button> </div> <div class="column"></div> <div class="column"> @@ -48,9 +31,4 @@ hx-post="/confirm" hx-target="#games">Start Round</button> </div> - <div class="column"> - <button class="button is-dark is-responsive" - hx-post="/clear" - hx-target="#games">Reset</button> - </div> </div>
\ No newline at end of file diff --git a/src/match_up/templates/games.j2 b/src/match_up/templates/games.j2 index d5afef1..46150f7 100644 --- a/src/match_up/templates/games.j2 +++ b/src/match_up/templates/games.j2 @@ -9,12 +9,12 @@ </thead> <tbody> {% for game in games %} - <tr {% if game.team1.player1 == 'RESERVED' %} class="is-danger" {% endif %}> + <tr {% if game.team1.player1.first == 'RESERVED' %} class="is-danger" {% endif %}> <th align="center" style="width: 8%;">{{ game.court }}</th> - <td style="width: 23%;">{{ game.team1.player1 }}</th> - <td style="width: 23%;">{{ game.team1.player2 }}</th> - <td style="width: 23%;">{{ game.team2.player1 }}</th> - <td style="width: 23%;">{{ game.team2.player2 }}</th> + <td style="width: 23%;"><strong>{{ game.team1.player1.first }}</strong>{{ " " + game.team1.player1.last }}</th> + <td style="width: 23%;"><strong>{{ game.team1.player2.first }}</strong>{{ " " + game.team1.player2.last }}</th> + <td style="width: 23%;"><strong>{{ game.team2.player1.first }}</strong>{{ " " + game.team2.player1.last }}</th> + <td style="width: 23%;"><strong>{{ game.team2.player2.first }}</strong>{{ " " + game.team2.player2.last }}</th> </tr> {% endfor %} </tbody> diff --git a/src/match_up/templates/macros.j2 b/src/match_up/templates/macros.j2 index dedbf48..7e1f6bd 100644 --- a/src/match_up/templates/macros.j2 +++ b/src/match_up/templates/macros.j2 @@ -1,17 +1,17 @@ -{% macro court(id, status) -%} +{% macro court_button(id, status) -%} <button class= "button is-responsive {% if status %}is-success{% else %}is-danger{% endif %}" hx-post="{{ "/court-toggle/" ~ id}}" hx-swap="outerHTML"> Court {{ id }}</button> {%- endmacro %} -{% macro player(id, status, name) -%} -<button class="button is-responsive {% if status %}is-success{% else %}is-danger {% endif %}" - hx-post="{{ "/player-toggle/" ~ id}}" - hx-swap="outerHTML"> {{ name }} </button> +{% macro player_button(player) -%} +<button class="button is-responsive {% if player.status %}is-success{% else %}is-danger {% endif %}" + hx-post="{{ "/player-toggle/" ~ player.id}}" + hx-swap="outerHTML"> {{ player.first + " " + player.last}} </button> {%- endmacro %} -{% macro guests(count) -%} -<div class="control" id="guests"> +{% macro guests(count, level) -%} +<div class="control" id="{{level}}guests"> <input class="input is-responsive" type="text" style="text-align:center" value="{{ count }} Guests" disabled> </div> {%- endmacro %} diff --git a/src/match_up/templates/players.j2 b/src/match_up/templates/players.j2 index 0d1f96c..f25889c 100644 --- a/src/match_up/templates/players.j2 +++ b/src/match_up/templates/players.j2 @@ -1,4 +1,4 @@ -{% from "macros.j2" import header, navbar, footer, player %} +{% from "macros.j2" import header, navbar, footer, player_button %} <!doctype html> <html lang="en"> {{ header(url_for) }} @@ -7,10 +7,67 @@ {{ navbar(url_for) }} <div class="fixed-grid has-4-cols"> <div class="grid"> - {% for id, value in players.items() %} - <div class="cell"> {{ player(id, value.status, value.name) }} </div> + {% for player in players %} + <div class="cell"> {{ player_button(player) }} </div> {% endfor %} </div> + <div class="columns"> + <div class="column"> + <div class="field has-addons"> + <div class="control"> + <button class="button is-responsive is-dark" + hx-post="/decrement_low_guest" + hx-swap="outerHTML" + hx-target="#lowguests">-</button> + </div> + <div hx-get="/low_guests" hx-swap="outerHTML" hx-trigger="revealed"></div> + <div class="control"> + <button class="button is-responsive is-dark" + hx-post="/increment_low_guest" + hx-swap="outerHTML" + hx-target="#lowguests">+</button> + </div> + </div> + </div> + <div class="column"> + <div class="field has-addons"> + <div class="control"> + <button class="button is-responsive is-dark" + hx-post="/decrement_mid_guest" + hx-swap="outerHTML" + hx-target="#midguests">-</button> + </div> + <div hx-get="/mid_guests" hx-swap="outerHTML" hx-trigger="revealed"></div> + <div class="control"> + <button class="button is-responsive is-dark" + hx-post="/increment_mid_guest" + hx-swap="outerHTML" + hx-target="#midguests">+</button> + </div> + </div> + </div> + <div class="column"> + <div class="field has-addons"> + <div class="control"> + <button class="button is-responsive is-dark" + hx-post="/decrement_high_guest" + hx-swap="outerHTML" + hx-target="#highguests">-</button> + </div> + <div hx-get="/high_guests" hx-swap="outerHTML" hx-trigger="revealed"></div> + <div class="control"> + <button class="button is-responsive is-dark" + hx-post="/increment_high_guest" + hx-swap="outerHTML" + hx-target="#highguests">+</button> + </div> + </div> + </div> + <div class="column"> + <button class="button is-dark is-responsive" + hx-post="/reset_players">Reset!</button> + </div> + </div> {{ footer() }} </section> </body> diff --git a/src/match_up/tests/test_db.py b/src/match_up/tests/test_db.py new file mode 100644 index 0000000..5097090 --- /dev/null +++ b/src/match_up/tests/test_db.py @@ -0,0 +1,15 @@ +from match_up.model import PlayerList + + +def main(): + p = PlayerList() + print(p.get_player_status(1)) + print(p.toggle_player_status(1)) + print(p.get_player_status(1)) + print(p.get_all_players()) + print(p.get_possible_game_count()) + print(p.get_players()) + + +if __name__ == "__main__": + main() |
