From 60a489cfb3fb7a12a06212c2c7ed3c05cd0757b5 Mon Sep 17 00:00:00 2001 From: Karan Jayachandra Date: Fri, 25 Oct 2024 18:03:18 +0200 Subject: Moved the files into a folder structure --- src/match_up/utilities.py | 58 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/match_up/utilities.py (limited to 'src/match_up/utilities.py') diff --git a/src/match_up/utilities.py b/src/match_up/utilities.py new file mode 100644 index 0000000..7a9d55d --- /dev/null +++ b/src/match_up/utilities.py @@ -0,0 +1,58 @@ +from os import getenv +from time import time +from typing import Tuple +from random import shuffle +from dataclasses import dataclass + +SECS_IN_MIN = 60 + + +def _shuffle_two_lists(a: list, b: list) -> Tuple[list, list]: + indices = list(range(len(a))) + shuffle(indices) + a = [a[index] for index in indices] + b = [b[index] for index in indices] + return a, b + + +def _pick_from_list_after_sorting_other(a: list, b: list[int]) -> list: + indices = [i[0] for i in sorted(enumerate(b), key=lambda x: x[1])] + return [a[index] for index in indices] + + +def sample_list(indices: list, cost: list[int], count: int = None) -> list: + if count is None: + count = len(indices) + indices, cost = _shuffle_two_lists(indices, cost) + indices = _pick_from_list_after_sorting_other(indices, cost)[:count] + return indices + + +@dataclass +class Timer: + ref: float = None + running: bool = False + max_mins: float = float(getenv("TIMER")) + default: str = ("00", "00") + + def __post_init__(self): + self.max_seconds = self.max_mins * SECS_IN_MIN + + def start(self): + self.ref = time() + self.max_seconds + self.running = True + + def stop(self): + self.ref = None + self.running = False + + def get(self): + if not self.running: + return self.default + total_seconds = self.ref - time() + if total_seconds < 0: + self.stop() + return self.default + mins = f"{int(total_seconds // SECS_IN_MIN):02d}" + secs = f"{int(total_seconds % SECS_IN_MIN):02d}" + return mins, secs -- cgit v1.3.1 From 14df28d5b8d3d9d80f24c532e1c096f8ac997e5b Mon Sep 17 00:00:00 2001 From: Karan Jayachandra Date: Sat, 9 Nov 2024 18:49:40 +0100 Subject: Added the random name generation to be part of the package --- src/match_up/__init__.py | 12 ++++++++++-- src/match_up/controller.py | 6 +++--- src/match_up/gen_test_data.py | 32 -------------------------------- src/match_up/model.py | 36 ++++++++++++++++++++++++------------ src/match_up/utilities.py | 5 +++-- 5 files changed, 40 insertions(+), 51 deletions(-) delete mode 100644 src/match_up/gen_test_data.py (limited to 'src/match_up/utilities.py') diff --git a/src/match_up/__init__.py b/src/match_up/__init__.py index 1f625a7..3967cb9 100644 --- a/src/match_up/__init__.py +++ b/src/match_up/__init__.py @@ -1,9 +1,10 @@ from flask import Flask from dotenv import load_dotenv +from argparse import ArgumentParser from importlib.resources import path from match_up.controller import RoundGenerator from match_up.model import CourtList, PlayerList - +from match_up.utilities import Timer load_dotenv() with path("match_up", "static") as p: @@ -11,10 +12,17 @@ with path("match_up", "static") as p: with path("match_up", "templates") as p: template_folder = p app = Flask("Match Up! Backend", static_folder=static_folder, template_folder=template_folder) -rg = RoundGenerator(courts=CourtList(), players=PlayerList()) +rg = RoundGenerator() from match_up import routes def main(): + parser = ArgumentParser() + parser.add_argument("-t", "--timer", help="Total mins per round", type=float, default=12) + parser.add_argument("-c", "--courts", help="Total number of courts", type=int, default=12) + args = parser.parse_args() + global rg + rg.courts = CourtList(total=args.courts) + rg.timer = Timer(max_mins=args.timer) app.run() diff --git a/src/match_up/controller.py b/src/match_up/controller.py index 2f8bfaf..6188b3e 100644 --- a/src/match_up/controller.py +++ b/src/match_up/controller.py @@ -1,6 +1,6 @@ from typing import Tuple from operator import attrgetter -from dataclasses import dataclass +from dataclasses import dataclass, field from match_up.utilities import sample_list, Timer from match_up.model import Team, Game, PlayerList, CourtList, MAX_LEVEL, PLAYER_PER_COURT @@ -43,8 +43,8 @@ def _create_games(courts: dict, players: list[dict], levels: int): @dataclass class RoundGenerator: - courts: CourtList - players: PlayerList + courts: CourtList = field(default_factory=lambda: CourtList()) + players: PlayerList = field(default_factory=lambda: PlayerList()) round: int = 0 proposal: tuple = tuple() diff --git a/src/match_up/gen_test_data.py b/src/match_up/gen_test_data.py deleted file mode 100644 index a49594f..0000000 --- a/src/match_up/gen_test_data.py +++ /dev/null @@ -1,32 +0,0 @@ -from csv import writer -from random import randint -from names import get_full_name -from model import Player, MIN_LEVEL, MAX_LEVEL - -PLAYER_COUNT = 80 - - -def _generate_players(player_count: int) -> list[Player]: - players = [] - for _ in range(player_count): - random_name = get_full_name() - random_skill = randint(MIN_LEVEL, MAX_LEVEL) - players.append(Player(name=random_name, skill=random_skill)) - return players - - -def _write_players_to_csv(player_list: list[Player]) -> None: - with open("database_test.csv", "w", newline="") as file: - data_writer = writer(file) - data_writer.writerow(["name", "skill"]) - for player in player_list: - data_writer.writerow([player.name, player.skill]) - - -def main(player_count: int): - players = _generate_players(player_count) - _write_players_to_csv(players) - - -if __name__ == "__main__": - main(PLAYER_COUNT) diff --git a/src/match_up/model.py b/src/match_up/model.py index c652ce8..592d60f 100644 --- a/src/match_up/model.py +++ b/src/match_up/model.py @@ -1,11 +1,13 @@ -from os import getenv from math import floor from csv import reader from typing import Tuple +from names import get_full_name from dataclasses import dataclass +from random import randint, getrandbits MIN_LEVEL = 1 MAX_LEVEL = 10 +DEFAULT_COURTS = 12 PLAYER_PER_COURT = 4 @@ -28,10 +30,9 @@ class Game: team1: Team team2: Team - @dataclass class CourtList: - total: int = int(getenv("COURTS")) + total: int = DEFAULT_COURTS def __post_init__(self) -> None: self.courts = {i + 1: True for i in range(self.total)} @@ -52,21 +53,32 @@ class CourtList: inactive_courts = {k: v for k, v in self.courts.items() if not v} 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 = getenv("DATABASE") + location: str = None guests: int = 0 def __post_init__(self) -> None: - with open("data/" + self.location, newline="") as file: - pointer = reader(file) - next(pointer, None) - players = { - id: Player(row[0], int(row[1])) for id, row in enumerate(pointer) - } - self.guest_id_start = len(players) + 1 - self.players = players + 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 increment_guests(self): self.guests = self.guests + 1 diff --git a/src/match_up/utilities.py b/src/match_up/utilities.py index 7a9d55d..8a905ba 100644 --- a/src/match_up/utilities.py +++ b/src/match_up/utilities.py @@ -1,10 +1,11 @@ -from os import getenv from time import time from typing import Tuple from random import shuffle from dataclasses import dataclass + SECS_IN_MIN = 60 +DEFAULT_TIMER = 12.0 def _shuffle_two_lists(a: list, b: list) -> Tuple[list, list]: @@ -30,9 +31,9 @@ def sample_list(indices: list, cost: list[int], count: int = None) -> list: @dataclass class Timer: + max_mins: float = DEFAULT_TIMER ref: float = None running: bool = False - max_mins: float = float(getenv("TIMER")) default: str = ("00", "00") def __post_init__(self): -- cgit v1.3.1 From e8ae44bebdd072b7b555cca5e0b8389d0eb3dbfd Mon Sep 17 00:00:00 2001 From: Karan Jayachandra Date: Fri, 15 Nov 2024 13:08:05 +0100 Subject: Removed the timer from the backend --- src/match_up/__init__.py | 3 --- src/match_up/controller.py | 4 +--- src/match_up/routes.py | 7 ------- src/match_up/utilities.py | 30 ------------------------------ 4 files changed, 1 insertion(+), 43 deletions(-) (limited to 'src/match_up/utilities.py') diff --git a/src/match_up/__init__.py b/src/match_up/__init__.py index 25b4d73..e6c472a 100644 --- a/src/match_up/__init__.py +++ b/src/match_up/__init__.py @@ -2,7 +2,6 @@ from flask import Flask from waitress import serve from argparse import ArgumentParser from importlib.resources import path -from match_up.utilities import Timer from match_up.controller import RoundGenerator from match_up.model import CourtList, PlayerList @@ -22,12 +21,10 @@ from match_up import routes def main(): parser = ArgumentParser() - parser.add_argument("-t", "--timer", help="Min per round", type=float, default=12) parser.add_argument("-c", "--courts", help="Total courts", type=int, default=12) parser.add_argument("-d", "--database", help="CSV database", default=None) args = parser.parse_args() global rg rg.courts = CourtList(total=args.courts) - rg.timer = Timer(max_mins=args.timer) rg.players = PlayerList(csv_location=args.database) serve(app, port=80) diff --git a/src/match_up/controller.py b/src/match_up/controller.py index 3b4a6f9..aa72b12 100644 --- a/src/match_up/controller.py +++ b/src/match_up/controller.py @@ -1,7 +1,7 @@ from typing import Tuple from operator import attrgetter from dataclasses import dataclass, field -from match_up.utilities import sample_list, Timer +from match_up.utilities import sample_list from match_up.model import ( Team, Game, @@ -72,7 +72,6 @@ class RoundGenerator: proposal: tuple = tuple() def __post_init__(self) -> None: - self.timer = Timer() self.games = _create_placeholders(self.courts.get_courts(), "---") def _separate_courts(self, courts: dict) -> Tuple[dict, dict]: @@ -123,5 +122,4 @@ class RoundGenerator: self.players.increment_game_count(identifiers) self.proposal = tuple() self.round += 1 - self.timer.start() return self.games diff --git a/src/match_up/routes.py b/src/match_up/routes.py index 95abe9e..f64eafa 100644 --- a/src/match_up/routes.py +++ b/src/match_up/routes.py @@ -10,13 +10,6 @@ def home(): return re.get_template("index.j2").render(url_for=url_for) -@app.route("/time", methods=["GET"]) -def get_time(): - mins, secs = rg.timer.get() - t = re.from_string('{% from "macros.j2" import timer %}{{timer(mins, secs)}}') - return render_template(t, mins=mins, secs=secs) - - @app.route("/control", methods=["GET"]) def get_controls(): return render_template("controls.j2", courts=rg.courts.get_courts()) diff --git a/src/match_up/utilities.py b/src/match_up/utilities.py index 8a905ba..8aadfd2 100644 --- a/src/match_up/utilities.py +++ b/src/match_up/utilities.py @@ -27,33 +27,3 @@ def sample_list(indices: list, cost: list[int], count: int = None) -> list: indices, cost = _shuffle_two_lists(indices, cost) indices = _pick_from_list_after_sorting_other(indices, cost)[:count] return indices - - -@dataclass -class Timer: - max_mins: float = DEFAULT_TIMER - ref: float = None - running: bool = False - default: str = ("00", "00") - - def __post_init__(self): - self.max_seconds = self.max_mins * SECS_IN_MIN - - def start(self): - self.ref = time() + self.max_seconds - self.running = True - - def stop(self): - self.ref = None - self.running = False - - def get(self): - if not self.running: - return self.default - total_seconds = self.ref - time() - if total_seconds < 0: - self.stop() - return self.default - mins = f"{int(total_seconds // SECS_IN_MIN):02d}" - secs = f"{int(total_seconds % SECS_IN_MIN):02d}" - return mins, secs -- cgit v1.3.1 From 6ca047f36fb688cdb72f0170e0fb3aaf3701ff95 Mon Sep 17 00:00:00 2001 From: Karan Jayachandra Date: Fri, 15 Nov 2024 14:55:05 +0100 Subject: Added multiple levels for the guests --- src/match_up/model.py | 35 ++++++++++++++----- src/match_up/routes.py | 72 ++++++++++++--------------------------- src/match_up/templates/macros.j2 | 22 ++++++++++-- src/match_up/templates/players.j2 | 68 +++++++----------------------------- src/match_up/utilities.py | 1 - 5 files changed, 78 insertions(+), 120 deletions(-) (limited to 'src/match_up/utilities.py') diff --git a/src/match_up/model.py b/src/match_up/model.py index b1e77f5..9b2dce7 100644 --- a/src/match_up/model.py +++ b/src/match_up/model.py @@ -10,7 +10,6 @@ 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 DEFAULT_COURTS = 12 @@ -104,8 +103,21 @@ class CourtList: return active_courts, inactive_courts +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}" + + class PlayerList: - guests: dict = {"low": 0, "mid": 0, "high": 0} + guests: dict = {1: 0, 3: 0, 5: 0} def __init__(self, csv_location: str = None) -> None: self._init_database() @@ -122,15 +134,15 @@ class PlayerList: Base.metadata.create_all(bind=engine) self.session = sessionmaker(bind=engine)() - def increment_guests(self, level: str): + def increment_guests(self, level: int): self.guests[level] = self.guests[level] + 1 - return self.guests[level] + return _textualize_guest(level, self.guests[level]) - def decrement_guests(self, level: str): + def decrement_guests(self, level: int): if self.guests[level] == 0: return 0 self.guests[level] = self.guests[level] - 1 - return self.guests[level] + return _textualize_guest(level, self.guests[level]) def get_player_status(self, id: int) -> bool: return self.session.query(Player).filter(Player.id == id).first().status @@ -171,7 +183,7 @@ class PlayerList: status=True, count=rounds, ) - for i in range(1, self.guests["low"] + 1) + for i in range(1, self.guests[1] + 1) ] mid_guests = [ Player( @@ -182,7 +194,7 @@ class PlayerList: status=True, count=rounds, ) - for i in range(1, self.guests["mid"] + 1) + for i in range(1, self.guests[3] + 1) ] high_guests = [ Player( @@ -193,10 +205,15 @@ class PlayerList: status=True, count=rounds, ) - for i in range(1, self.guests["high"] + 1) + for i in range(1, self.guests[5] + 1) ] return active_players + low_guests + mid_guests + high_guests + def get_guests(self): + return { + level: _textualize_guest(level, self.guests[level]) for level in self.guests + } + def increment_game_count(self, player_ids: list[int]): players = self.session.query(Player).filter(Player.id.in_(player_ids)) for player in players: diff --git a/src/match_up/routes.py b/src/match_up/routes.py index 57e9812..14b36d2 100644 --- a/src/match_up/routes.py +++ b/src/match_up/routes.py @@ -15,58 +15,25 @@ def get_controls(): return render_template("controls.j2", courts=rg.courts.get_courts()) -@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.decrement_guests("low")) - - -@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.increment_guests("mid")) - - -@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.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("/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_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("/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"]) @@ -98,7 +65,10 @@ def toggle_player(player_request): @app.route("/players", methods=["GET"]) def get_list_of_players(): all_players = rg.players.get_all_players() - return re.get_template("players.j2").render(players=all_players, url_for=url_for) + guests = rg.players.get_guests() + return re.get_template("players.j2").render( + players=all_players, guests=guests, url_for=url_for + ) @app.route("/reset_players", methods=["POST"]) diff --git a/src/match_up/templates/macros.j2 b/src/match_up/templates/macros.j2 index 2a8ad1e..83fc823 100644 --- a/src/match_up/templates/macros.j2 +++ b/src/match_up/templates/macros.j2 @@ -10,9 +10,25 @@ hx-swap="outerHTML"> {{ player.first + " " + player.last}} {%- endmacro %} -{% macro guests(count, level) -%} -
- +{% macro guest_control(level, value) -%} +
+
+ +
+
+ +
+
+ +
{%- endmacro %} diff --git a/src/match_up/templates/players.j2 b/src/match_up/templates/players.j2 index f25889c..46ff172 100644 --- a/src/match_up/templates/players.j2 +++ b/src/match_up/templates/players.j2 @@ -1,72 +1,28 @@ -{% from "macros.j2" import header, navbar, footer, player_button %} +{% from "macros.j2" import header, navbar, footer, player_button, guest_control %} {{ header(url_for) }}
{{ navbar(url_for) }} +

Players

{% for player in players %}
{{ player_button(player) }}
{% endfor %}
+

Guests

-
-
-
- -
-
-
- -
-
-
-
-
-
- -
-
-
- -
-
-
-
-
-
- -
-
-
- -
-
-
-
- -
+ {% for level in guests %} +
{{ guest_control(level, guests[level]) }}
+ {% endfor %} +
+
+
+
+
{{ footer() }}
diff --git a/src/match_up/utilities.py b/src/match_up/utilities.py index 8aadfd2..56fb78e 100644 --- a/src/match_up/utilities.py +++ b/src/match_up/utilities.py @@ -1,7 +1,6 @@ from time import time from typing import Tuple from random import shuffle -from dataclasses import dataclass SECS_IN_MIN = 60 -- cgit v1.3.1 From 765fe232ac9fded73387d5cb9ba9b0a07f05b4e4 Mon Sep 17 00:00:00 2001 From: Karan Jayachandra Date: Fri, 15 Nov 2024 16:54:29 +0100 Subject: Added the history and reset functionality --- src/match_up/__init__.py | 12 +-- src/match_up/controller.py | 112 ++++++-------------- src/match_up/data.py | 70 +++++++++++++ src/match_up/model.py | 202 +++++++------------------------------ src/match_up/routes.py | 55 +++++----- src/match_up/templates/controls.j2 | 5 + src/match_up/utilities.py | 136 +++++++++++++++++++++++-- 7 files changed, 307 insertions(+), 285 deletions(-) create mode 100644 src/match_up/data.py (limited to 'src/match_up/utilities.py') diff --git a/src/match_up/__init__.py b/src/match_up/__init__.py index e6c472a..bccd0e7 100644 --- a/src/match_up/__init__.py +++ b/src/match_up/__init__.py @@ -3,17 +3,16 @@ from waitress import serve from argparse import ArgumentParser from importlib.resources import path from match_up.controller import RoundGenerator -from match_up.model import CourtList, PlayerList with path("match_up", "static") as p: static_folder = p with path("match_up", "templates") as p: template_folder = p -app = Flask( +_app = Flask( "Match Up! Backend", static_folder=static_folder, template_folder=template_folder ) -rg = RoundGenerator() +_rg = RoundGenerator() from match_up import routes @@ -21,10 +20,7 @@ from match_up import routes def main(): parser = ArgumentParser() - parser.add_argument("-c", "--courts", help="Total courts", type=int, default=12) parser.add_argument("-d", "--database", help="CSV database", default=None) args = parser.parse_args() - global rg - rg.courts = CourtList(total=args.courts) - rg.players = PlayerList(csv_location=args.database) - serve(app, port=80) + _rg.update_player_list(csv_location=args.database) + serve(_app, port=80) diff --git a/src/match_up/controller.py b/src/match_up/controller.py index aa72b12..8e89ffd 100644 --- a/src/match_up/controller.py +++ b/src/match_up/controller.py @@ -1,78 +1,18 @@ from typing import Tuple from operator import attrgetter -from dataclasses import dataclass, field -from match_up.utilities import sample_list -from match_up.model import ( - Team, - Game, - Player, - DisplayPlayer, - PlayerList, - CourtList, - MAX_LEVEL, - PLAYER_PER_COURT, -) +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 -def _create_placeholders(courts: dict, name: str) -> list[Game]: - 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(level: int, new_max: int) -> int: - new_level = round((new_max - 1) * (level / MAX_LEVEL)) + 1 - return new_level - - -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): - 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[Player], levels: int): - court_players = _create_groups_of_four_players(players, levels) - games = [] - 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 - - -@dataclass class RoundGenerator: - courts: CourtList = field(default_factory=lambda: CourtList()) - players: PlayerList = field(default_factory=lambda: PlayerList()) - round: int = 0 - proposal: tuple = tuple() + proposed_games: list[Game] = [] + proposed_players: list[Player] = [] - def __post_init__(self) -> None: - self.games = _create_placeholders(self.courts.get_courts(), "---") + def __init__(self) -> None: + self.courts = CourtList() + self.players = PlayerList() + self.reset() def _separate_courts(self, courts: dict) -> Tuple[dict, dict]: possible_games = min(len(courts), self.players.get_possible_game_count()) @@ -89,7 +29,9 @@ class RoundGenerator: games = reserved_games + blank_games return games - def _create_active_games(self, total_levels: int): + def _create_active_games( + self, total_levels: int + ) -> Tuple[list[Game], list[Player]]: active_courts, _ = self.courts.separate_courts() used_courts, _ = self._separate_courts(active_courts) required_player_count = len(used_courts) * PLAYER_PER_COURT @@ -98,28 +40,38 @@ class RoundGenerator: games = _create_games(used_courts, selected_players, total_levels) return games, selected_players - def _generate_proposal(self, total_levels: int) -> Tuple[list[Game], dict]: + 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")) return games, selected_players + def update_player_list(self, csv_location): + self.players = PlayerList(csv_location) + def clear(self) -> list[Game]: - self.proposal = tuple() + self.proposed_games = [] + self.proposed_players = [] return self.games def propose(self, levels: int) -> list[Game]: - self.proposal = self._generate_proposal(levels) - return self.proposal[0] + proposal = self._generate_proposal(levels) + self.proposed_games = proposal[0] + self.proposed_players = proposal[1] + return self.proposed_games def confirm(self) -> list[Game]: - proposed_games, proposed_players = self.proposal - if self.games == proposed_games or len(proposed_games) == 0: + if len(self.proposed_games) == 0: return self.games - self.games = proposed_games - identifiers = [player.id for player in proposed_players] + self.games = self.proposed_games + identifiers = [int(player.id) for player in self.proposed_players] self.players.increment_game_count(identifiers) - self.proposal = tuple() self.round += 1 - return self.games + return self.clear() + + def reset(self): + self.round = 0 + self.games = _create_placeholders(self.courts.get_courts(), "---") + self.players.reset_game_count() + return self.games diff --git a/src/match_up/data.py b/src/match_up/data.py new file mode 100644 index 0000000..62aa5d8 --- /dev/null +++ b/src/match_up/data.py @@ -0,0 +1,70 @@ +from dataclasses import dataclass +from sqlalchemy import Column, String, Integer +from sqlalchemy.ext.declarative import declarative_base + + +MIN_LEVEL = 1 +MAX_LEVEL = 10 +PLAYER_PER_COURT = 4 + +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, + first: str = "", + last: str = "", + level: int = 0, + status: bool = True, + count: int = 0, + ): + 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 = int(player.id) + self.first = str(player.first) + self.last = str(player.last) + self.status = bool(player.status) + + def __repr__(self): + status = "active" if self.status else "inactive" + return f"{self.first} {self.last} is {status}" + + +@dataclass +class Team: + player1: DisplayPlayer + player2: DisplayPlayer + + +@dataclass +class Game: + court: int + team1: Team + team2: Team diff --git a/src/match_up/model.py b/src/match_up/model.py index 9b2dce7..abce73c 100644 --- a/src/match_up/model.py +++ b/src/match_up/model.py @@ -3,85 +3,25 @@ from typing import Tuple 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 -DEFAULT_COURTS = 12 -PLAYER_PER_COURT = 4 - - -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: DisplayPlayer - player2: DisplayPlayer - - -@dataclass -class Game: - court: int - team1: Team - team2: Team +from sqlalchemy import create_engine, not_ +from match_up.utilities import ( + generate_guests, + _init_database_from_csv, + _init_random_database, + _textualize_guest, +) +from match_up.data import ( + Player, + DisplayPlayer, + PLAYER_PER_COURT, + BASE, +) @dataclass class CourtList: - total: int = DEFAULT_COURTS + total: int = 12 def __post_init__(self) -> None: self.courts = {i + 1: True for i in range(self.total)} @@ -89,7 +29,7 @@ class CourtList: def get_court_status(self, court_number: int) -> bool: return self.courts[court_number] - def toggle_court_status(self, court_number: int) -> None: + def toggle_court_status(self, court_number: int) -> bool: new_status = not self.courts[court_number] self.courts[court_number] = new_status return new_status @@ -103,46 +43,36 @@ class CourtList: return active_courts, inactive_courts -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}" - - class PlayerList: - guests: dict = {1: 0, 3: 0, 5: 0} + guest_count: dict = {1: 0, 3: 0, 5: 0} - def __init__(self, csv_location: str = None) -> None: + def __init__(self, csv_location=None) -> None: + with path("match_up") as p: + self.db_location = join(p, "players.db") + db_file_found = True + if not isfile(self.db_location): + db_file_found = False self._init_database() if csv_location is not None: - self._init_database_from_csv(read_csv(csv_location)) + _init_database_from_csv(self.session, csv_location) else: - if not isfile(self.db_location): - self._init_random_database() + if not db_file_found: + _init_random_database(self.session) def _init_database(self): - with path("match_up") as p: - self.db_location = join(p, "players.db") engine = create_engine("sqlite:///" + self.db_location, echo=True) - Base.metadata.create_all(bind=engine) + BASE.metadata.create_all(bind=engine) self.session = sessionmaker(bind=engine)() def increment_guests(self, level: int): - self.guests[level] = self.guests[level] + 1 - return _textualize_guest(level, self.guests[level]) + 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.guests[level] == 0: + if self.guest_count[level] == 0: return 0 - self.guests[level] = self.guests[level] - 1 - return _textualize_guest(level, self.guests[level]) + 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 @@ -153,7 +83,7 @@ class PlayerList: self.session.commit() return DisplayPlayer(player) - def get_all_players(self) -> list[Player]: + def get_all_players(self) -> list[DisplayPlayer]: all_players = [ DisplayPlayer(p) for p in self.session.query(Player) @@ -167,51 +97,20 @@ class PlayerList: possible_games = floor(total_players / PLAYER_PER_COURT) return possible_games - def get_players(self, rounds: int = 0) -> list[Player]: + def get_players(self) -> list[Player]: active_players = ( self.session.query(Player) .order_by(Player.first, Player.last) .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[1] + 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[3] + 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[5] + 1) - ] - return active_players + low_guests + mid_guests + high_guests + guests = generate_guests(self.guest_count) + return active_players + guests def get_guests(self): return { - level: _textualize_guest(level, self.guests[level]) for level in self.guests + level: _textualize_guest(level, self.guest_count[level]) + for level in self.guest_count } def increment_game_count(self, player_ids: list[int]): @@ -231,30 +130,3 @@ class PlayerList: for player in players: player.status = 0 self.session.commit() - - def _init_database_from_csv(self, data: DataFrame) -> None: - self.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, - ) - 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 14b36d2..c0e77aa 100644 --- a/src/match_up/routes.py +++ b/src/match_up/routes.py @@ -1,94 +1,99 @@ -from match_up import app, rg +from match_up import _app, _rg from jinja2 import Environment, PackageLoader from flask import render_template, request, url_for, Response re = Environment(loader=PackageLoader("match_up")) -@app.route("/", methods=["GET"]) +@_app.route("/", methods=["GET"]) def home(): return re.get_template("index.j2").render(url_for=url_for) -@app.route("/control", methods=["GET"]) +@_app.route("/control", methods=["GET"]) def get_controls(): - return render_template("controls.j2", courts=rg.courts.get_courts()) + return render_template("controls.j2", courts=_rg.courts.get_courts()) -@app.route("/increment_guest", methods=["POST"]) +@_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) + value = _rg.players.increment_guests(level) return render_template(t, level=level, value=value) -@app.route("/decrement_guest", methods=["POST"]) +@_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) + value = _rg.players.decrement_guests(level) return render_template(t, level=level, value=value) -@app.route("/propose", methods=["POST"]) +@_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") + return render_template("games.j2", games=_rg.propose(levels), title="Proposal") -@app.route("/confirm", methods=["POST"]) +@_app.route("/confirm", methods=["POST"]) def confirm_games(): - return render_template("games.j2", games=rg.confirm(), title=f"Round: {rg.round}") + return render_template("games.j2", games=_rg.confirm(), title=f"Round: {_rg.round}") -@app.route("/clear", methods=["POST"]) +@_app.route("/clear", methods=["POST"]) def clear_games(): - return render_template("games.j2", games=rg.clear(), title=f"Round: {rg.round}") + return render_template("games.j2", games=_rg.clear(), title=f"Round: {_rg.round}") -@app.route("/player-toggle/", methods=["POST"]) +@_app.route("/reset", methods=["POST"]) +def reset_games(): + return render_template("games.j2", games=_rg.reset(), title=f"Round: {_rg.round}") + + +@_app.route("/player-toggle/", methods=["POST"]) def toggle_player(player_request): id = int(player_request) - player = rg.players.toggle_player_status(id) + player = _rg.players.toggle_player_status(id) t = re.from_string( '{% from "macros.j2" import player_button %}{{player_button(player)}}' ) return render_template(t, player=player) -@app.route("/players", methods=["GET"]) +@_app.route("/players", methods=["GET"]) def get_list_of_players(): - all_players = rg.players.get_all_players() - guests = rg.players.get_guests() + all_players = _rg.players.get_all_players() + guests = _rg.players.get_guests() return re.get_template("players.j2").render( players=all_players, guests=guests, url_for=url_for ) -@app.route("/reset_players", methods=["POST"]) +@_app.route("/reset_players", methods=["POST"]) def reset_all_players(): - rg.players.reset_all_players() + _rg.players.reset_all_players() response = Response("Players reset!") response.headers["HX-Refresh"] = "true" return response -@app.route("/court-toggle/", methods=["POST"]) +@_app.route("/court-toggle/", methods=["POST"]) def toggle_court(court_number): id = int(court_number) - status = rg.courts.toggle_court_status(id) + status = _rg.courts.toggle_court_status(id) t = re.from_string( '{% from "macros.j2" import court_button %}{{ court_button(id, status) }}' ) return render_template(t, id=id, status=status) -@app.route("/courts", methods=["GET"]) +@_app.route("/courts", methods=["GET"]) def get_list_of_courts(): - return render_template("courts.j2", courts=rg.courts.get_courts()) + return render_template("courts.j2", courts=_rg.courts.get_courts()) diff --git a/src/match_up/templates/controls.j2 b/src/match_up/templates/controls.j2 index e9e37f3..57a605e 100644 --- a/src/match_up/templates/controls.j2 +++ b/src/match_up/templates/controls.j2 @@ -31,4 +31,9 @@ hx-post="/confirm" hx-target="#games">Start Round
+
+ +
\ No newline at end of file diff --git a/src/match_up/utilities.py b/src/match_up/utilities.py index 56fb78e..db7c76a 100644 --- a/src/match_up/utilities.py +++ b/src/match_up/utilities.py @@ -1,10 +1,17 @@ -from time import time from typing import Tuple -from random import shuffle - - -SECS_IN_MIN = 60 -DEFAULT_TIMER = 12.0 +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, +) def _shuffle_two_lists(a: list, b: list) -> Tuple[list, list]: @@ -20,9 +27,124 @@ def _pick_from_list_after_sorting_other(a: list, b: list[int]) -> list: return [a[index] for index in indices] -def sample_list(indices: list, cost: list[int], count: int = None) -> list: +def sample_list(indices: list, cost: list[int], count=None) -> list: if count is None: count = len(indices) indices, cost = _shuffle_two_lists(indices, cost) indices = _pick_from_list_after_sorting_other(indices, cost)[:count] return indices + + +def _create_placeholders(courts: dict, name: str) -> list[Game]: + dummy_player = Player(id=-1, 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(level: int, new_max: int) -> int: + new_level = round((new_max - 1) * (level / MAX_LEVEL)) + 1 + return new_level + + +def _select_players(players: list[Player], count: int) -> list[Player]: + identifiers = [player.id for player in players] + play_count = [int(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(int(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): + 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[Player], levels: int) -> list[Game]: + court_players = _create_groups_of_four_players(players, levels) + games = [] + 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 + + +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]), + ) + 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}" + + +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 _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() -- cgit v1.3.1