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/controller.py | 119 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 src/match_up/controller.py (limited to 'src/match_up/controller.py') diff --git a/src/match_up/controller.py b/src/match_up/controller.py new file mode 100644 index 0000000..e6dc827 --- /dev/null +++ b/src/match_up/controller.py @@ -0,0 +1,119 @@ +from typing import Tuple +from operator import attrgetter +from dataclasses import dataclass +from utilities import sample_list, Timer +from model import Team, Game, PlayerList, CourtList, MAX_LEVEL, PLAYER_PER_COURT + + +def _create_placeholders(courts: dict, name: str) -> list[Game]: + dummy_team = Team(player1=name, player2=name) + 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 _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 = [] + 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 + + +def _create_games(courts: dict, players: list[dict], levels: int): + grouped_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) + games.append(Game(court, team1, team2)) + return games + + +@dataclass +class RoundGenerator: + courts: CourtList + players: PlayerList + round: int = 0 + proposal: tuple = tuple() + + 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] + + def _separate_courts(self, courts: dict) -> Tuple[dict, dict]: + possible_games = min(len(courts), self.players.get_possible_game_count()) + courts_as_list = list(courts.items()) + used_courts = dict(courts_as_list[:possible_games]) + 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") + _, unused_courts = self._separate_courts(active_courts) + blank_games = _create_placeholders(unused_courts, "---") + games = reserved_games + blank_games + return games + + def _create_active_games(self, total_levels: int): + 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) + games = _create_games(used_courts, selected_players, total_levels) + return games, selected_players + + def _generate_proposal(self, total_levels: int) -> Tuple[list[Game], dict]: + 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 clear(self) -> list[Game]: + self.proposal = tuple() + return self.games + + def propose(self, levels: int) -> list[Game]: + self.proposal = self._generate_proposal(levels) + return self.proposal[0] + + def confirm(self) -> list[Game]: + proposed_games, proposed_players = self.proposal + 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 + self.proposal = tuple() + self.round += 1 + self.timer.start() + return self.games -- cgit v1.3.1 From 2a390e99e03fe538169aa6d7c23669eaa5ece467 Mon Sep 17 00:00:00 2001 From: Karan Jayachandra Date: Fri, 25 Oct 2024 18:24:13 +0200 Subject: Added the requirements on the static files --- pyproject.toml | 19 +++++++++- src/match_up/__init__.py | 93 ++++++++++++++++++++++++++++++++++++++++++++++ src/match_up/app.py | 90 -------------------------------------------- src/match_up/controller.py | 4 +- 4 files changed, 112 insertions(+), 94 deletions(-) create mode 100644 src/match_up/__init__.py delete mode 100644 src/match_up/app.py (limited to 'src/match_up/controller.py') diff --git a/pyproject.toml b/pyproject.toml index cae5554..9ef21d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,10 @@ +[build-system] +requires = ["setuptools>=64", "setuptools_scm>=8"] +build-backend = "setuptools.build_meta" + [project] name = "match_up" -version = "0.1" +dynamic = ["version"] authors = [ { name="Karan Jayachandra", email="mail@karanjayachandra.com" }, ] @@ -15,4 +19,15 @@ classifiers = [ [project.urls] Homepage = "https://gitlab.com/KaranJayachandra/match_up" -Issues = "https://gitlab.com/KaranJayachandra/match_up/-/issues" \ No newline at end of file +Issues = "https://gitlab.com/KaranJayachandra/match_up/-/issues" + + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +"*" = ["*.txt"] +mypkg1 = ["data1.rst"] + +[project.scripts] +match_up = "match_up:main" \ No newline at end of file diff --git a/src/match_up/__init__.py b/src/match_up/__init__.py new file mode 100644 index 0000000..badf3f0 --- /dev/null +++ b/src/match_up/__init__.py @@ -0,0 +1,93 @@ +from dotenv import load_dotenv +from match_up.controller import RoundGenerator +from match_up.model import CourtList, PlayerList +from jinja2 import FileSystemLoader, Environment +from flask import Flask, render_template, request + + +load_dotenv() +app = Flask("Match Up! Backend") +rg = RoundGenerator(courts=CourtList(), players=PlayerList()) +re = Environment(loader=FileSystemLoader("templates")) + + +@app.route("/", methods=["GET"]) +def home(): + return render_template("index.j2") + + +@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()) + + +@app.route("/guests", methods=["GET"]) +def get_guests(): + t = re.from_string('{% from "macros.j2" import guests %}{{guests(count)}}') + return render_template(t, count=rg.players.guests) + + +@app.route("/decrement", methods=["POST"]) +def increment_guests(): + t = re.from_string('{% from "macros.j2" import guests %}{{guests(count)}}') + return render_template(t, count=rg.players.decrement_guests()) + + +@app.route("/increment", methods=["POST"]) +def decrement_guests(): + t = re.from_string('{% from "macros.j2" import guests %}{{guests(count)}}') + return render_template(t, count=rg.players.increment_guests()) + + +@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") + + +@app.route("/confirm", methods=["POST"]) +def confirm_games(): + return render_template("games.j2", games=rg.confirm(), title=f"Round: {rg.round}") + + +@app.route("/clear", methods=["POST"]) +def clear_games(): + return render_template("games.j2", games=rg.clear(), title=f"Round: {rg.round}") + + +@app.route("/player-toggle/", methods=["POST"]) +def toggle_player(player_request): + id = int(player_request) + status, name = rg.players.toggle_player_status(id) + t = re.from_string( + '{% from "macros.j2" import player %}{{player(id, status, name)}}' + ) + return render_template(t, id=id, status=status, name=name) + + +@app.route("/players", methods=["GET"]) +def get_list_of_players(): + return render_template("players.j2", players=rg.players.get_players()) + + +@app.route("/court-toggle/", 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) }}') + return render_template(t, id=id, status=status) + + +@app.route("/courts", methods=["GET"]) +def get_list_of_courts(): + return render_template("courts.j2", courts=rg.courts.get_courts()) + +def main(): + app.run() diff --git a/src/match_up/app.py b/src/match_up/app.py deleted file mode 100644 index cb790f3..0000000 --- a/src/match_up/app.py +++ /dev/null @@ -1,90 +0,0 @@ -from dotenv import load_dotenv -from controller import RoundGenerator -from model import CourtList, PlayerList -from jinja2 import FileSystemLoader, Environment -from flask import Flask, render_template, request - - -load_dotenv() -app = Flask("Match Up! Backend") -rg = RoundGenerator(courts=CourtList(), players=PlayerList()) -re = Environment(loader=FileSystemLoader("templates")) - - -@app.route("/", methods=["GET"]) -def home(): - return render_template("index.j2") - - -@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()) - - -@app.route("/guests", methods=["GET"]) -def get_guests(): - t = re.from_string('{% from "macros.j2" import guests %}{{guests(count)}}') - return render_template(t, count=rg.players.guests) - - -@app.route("/decrement", methods=["POST"]) -def increment_guests(): - t = re.from_string('{% from "macros.j2" import guests %}{{guests(count)}}') - return render_template(t, count=rg.players.decrement_guests()) - - -@app.route("/increment", methods=["POST"]) -def decrement_guests(): - t = re.from_string('{% from "macros.j2" import guests %}{{guests(count)}}') - return render_template(t, count=rg.players.increment_guests()) - - -@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") - - -@app.route("/confirm", methods=["POST"]) -def confirm_games(): - return render_template("games.j2", games=rg.confirm(), title=f"Round: {rg.round}") - - -@app.route("/clear", methods=["POST"]) -def clear_games(): - return render_template("games.j2", games=rg.clear(), title=f"Round: {rg.round}") - - -@app.route("/player-toggle/", methods=["POST"]) -def toggle_player(player_request): - id = int(player_request) - status, name = rg.players.toggle_player_status(id) - t = re.from_string( - '{% from "macros.j2" import player %}{{player(id, status, name)}}' - ) - return render_template(t, id=id, status=status, name=name) - - -@app.route("/players", methods=["GET"]) -def get_list_of_players(): - return render_template("players.j2", players=rg.players.get_players()) - - -@app.route("/court-toggle/", 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) }}') - return render_template(t, id=id, status=status) - - -@app.route("/courts", methods=["GET"]) -def get_list_of_courts(): - return render_template("courts.j2", courts=rg.courts.get_courts()) diff --git a/src/match_up/controller.py b/src/match_up/controller.py index e6dc827..2f8bfaf 100644 --- a/src/match_up/controller.py +++ b/src/match_up/controller.py @@ -1,8 +1,8 @@ from typing import Tuple from operator import attrgetter from dataclasses import dataclass -from utilities import sample_list, Timer -from model import Team, Game, PlayerList, CourtList, MAX_LEVEL, PLAYER_PER_COURT +from match_up.utilities import sample_list, Timer +from match_up.model import Team, Game, PlayerList, CourtList, MAX_LEVEL, PLAYER_PER_COURT def _create_placeholders(courts: dict, name: str) -> list[Game]: -- 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/controller.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 aa73a7618075b54940d32b7b1e0e751b301bee4d Mon Sep 17 00:00:00 2001 From: Karan Jayachandra Date: Sat, 9 Nov 2024 18:52:45 +0100 Subject: Formatted the package with black --- src/match_up/__init__.py | 15 +++++++++++---- src/match_up/controller.py | 9 ++++++++- src/match_up/model.py | 19 +++++++++++++------ src/match_up/routes.py | 11 ++++++----- 4 files changed, 38 insertions(+), 16 deletions(-) (limited to 'src/match_up/controller.py') diff --git a/src/match_up/__init__.py b/src/match_up/__init__.py index 3967cb9..996bf9a 100644 --- a/src/match_up/__init__.py +++ b/src/match_up/__init__.py @@ -2,25 +2,32 @@ from flask import Flask from dotenv import load_dotenv 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 -from match_up.utilities import Timer load_dotenv() with path("match_up", "static") as p: static_folder = p with path("match_up", "templates") as p: template_folder = p -app = Flask("Match Up! Backend", static_folder=static_folder, template_folder=template_folder) +app = Flask( + "Match Up! Backend", static_folder=static_folder, template_folder=template_folder +) 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) + 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) diff --git a/src/match_up/controller.py b/src/match_up/controller.py index 6188b3e..a21fced 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 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 +from match_up.model import ( + Team, + Game, + PlayerList, + CourtList, + MAX_LEVEL, + PLAYER_PER_COURT, +) def _create_placeholders(courts: dict, name: str) -> list[Game]: diff --git a/src/match_up/model.py b/src/match_up/model.py index 592d60f..5d6c0f5 100644 --- a/src/match_up/model.py +++ b/src/match_up/model.py @@ -30,6 +30,7 @@ class Game: team1: Team team2: Team + @dataclass class CourtList: total: int = DEFAULT_COURTS @@ -53,21 +54,27 @@ 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) - } + 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)} + 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 diff --git a/src/match_up/routes.py b/src/match_up/routes.py index 524d50b..319b111 100644 --- a/src/match_up/routes.py +++ b/src/match_up/routes.py @@ -4,10 +4,10 @@ from flask import render_template, request, url_for re = Environment(loader=PackageLoader("match_up")) + @app.route("/", methods=["GET"]) def home(): - template = re.get_template("index.j2").render(url_for=url_for) - return template + return re.get_template("index.j2").render(url_for=url_for) @app.route("/time", methods=["GET"]) @@ -68,8 +68,9 @@ def toggle_player(player_request): @app.route("/players", methods=["GET"]) def get_list_of_players(): - template = re.get_template("players.j2").render(players=rg.players.get_players(), url_for=url_for) - return template + return re.get_template("players.j2").render( + players=rg.players.get_players(), url_for=url_for + ) @app.route("/court-toggle/", methods=["POST"]) @@ -82,4 +83,4 @@ def toggle_court(court_number): @app.route("/courts", methods=["GET"]) def get_list_of_courts(): - return render_template("courts.j2", courts=rg.courts.get_courts()) \ No newline at end of file + return render_template("courts.j2", courts=rg.courts.get_courts()) -- cgit v1.3.1