diff options
| -rw-r--r-- | app.py | 58 | ||||
| -rw-r--r-- | controller.py | 102 | ||||
| -rw-r--r-- | model.py | 185 | ||||
| -rw-r--r-- | templates/courts.j2 | 5 | ||||
| -rw-r--r-- | templates/games.j2 | 19 | ||||
| -rw-r--r-- | templates/index.j2 | 115 | ||||
| -rw-r--r-- | templates/player.j2 | 7 | ||||
| -rw-r--r-- | templates/players.j2 | 4 |
8 files changed, 251 insertions, 244 deletions
@@ -1,20 +1,18 @@ from os import getenv +from controller import Session from dotenv import load_dotenv -from flask import Flask, abort, render_template -from model import CourtList, PlayerList, GameList +from model import CourtList, PlayerList +from flask import Flask, render_template, request + load_dotenv() -PLAYERS = PlayerList(location="test_data_large.csv") -COURTS = CourtList(12) -GAME_GENERATOR = GameList() -GAME_GENERATOR.clear(COURTS) +S = Session(courts=CourtList(12), players=PlayerList("test_data_large.csv")) app = Flask("Match Up! Backend") -env_config = getenv("PROD_APP_SETTINGS", "config.DevelopmentConfig") -app.config.from_object(env_config) +app.config.from_object(getenv("PROD_APP_SETTINGS", "config.DevelopmentConfig")) @app.route("/", methods=["GET"]) @@ -22,54 +20,36 @@ def home(): return render_template("index.j2") -@app.route("/random-games", methods=["POST"]) +@app.route("/propose", methods=["POST"]) def get_random_games(): - print("Creating new games...") - GAME_GENERATOR.create_random_games(COURTS, PLAYERS) - return render_template("games.j2", games=GAME_GENERATOR.games) - - -@app.route("/skilled-games", methods=["POST"]) -def get_skilled_games(): - print("Creating new games...") - GAME_GENERATOR.create_skilled_games(COURTS, PLAYERS) - return render_template("games.j2", games=GAME_GENERATOR.games) + levels = int(request.form["type"].split()[0]) + return render_template("games.j2", title="Proposal", games=S.propose_round(levels)) -@app.route("/clear-games", methods=["POST"]) +@app.route("/clear", methods=["POST"]) def clear_games(): - print("Clearing games...") - GAME_GENERATOR.clear(COURTS) - return render_template("games.j2", games=GAME_GENERATOR.games) - - -@app.route("/game-list", methods=["GET"]) -def get_list_of_games(): - return render_template("games.j2", games=GAME_GENERATOR.games) + return render_template("games.j2", title=f"Round: {S.round}", games=S.clear_round()) @app.route("/player-toggle/<player_request>", methods=["POST"]) def toggle_player(player_request): - selection = PLAYERS.get_player(player_request) - if len(selection) != 1: - abort(404) - PLAYERS.toggle_player(player_request) - selection = PLAYERS.get_player(player_request) - return render_template("player.j2", player=selection[0]) + id = int(player_request) + status, name = S.players.toggle_player_status(id) + return render_template("player.j2", id=id, status=status, name=name) -@app.route("/player-list", methods=["GET"]) +@app.route("/players", methods=["GET"]) def get_list_of_players(): - return render_template("players.j2", players=PLAYERS.players) + return render_template("players.j2", players=S.players.get_players()) @app.route("/court-toggle/<court_number>", methods=["POST"]) def toggle_court(court_number): id = int(court_number) - status = COURTS.toggle_court_status(id) + status = S.courts.toggle_court_status(id) return render_template("court.j2", id=id, status=status) -@app.route("/court-list", methods=["GET"]) +@app.route("/courts", methods=["GET"]) def get_list_of_courts(): - return render_template("courts.j2", courts=COURTS.courts) + return render_template("courts.j2", courts=S.courts.get_courts()) diff --git a/controller.py b/controller.py new file mode 100644 index 0000000..45ae2bf --- /dev/null +++ b/controller.py @@ -0,0 +1,102 @@ +from math import floor +from random import sample +from operator import attrgetter +from dataclasses import dataclass, field +from model import Team, Game, PlayerList, CourtList + +PLAYER_PER_COURT = 4 + + +def _create_placeholders(courts: dict, name: str): + dummy_team = Team(player1=name, player2=name) + return [Game(court, dummy_team, dummy_team) for court in courts] + + +def create_game(court: int, players: dict): + ids = list(players.keys()) + team1 = Team(player1=players[ids[0]].name, player2=players[ids[3]].name) + team2 = Team(player1=players[ids[1]].name, player2=players[ids[2]].name) + return Game(court, team1, team2) + + +def select_players(players: dict, count: int): + identifiers = sample(sorted(players), count) + return {id: players[id] for id in identifiers} + + +def get_random_combinations(players: dict) -> list[dict]: + ids = list(players.keys()) + clumps = [ + ids[i : i + PLAYER_PER_COURT] for i in range(0, len(players), PLAYER_PER_COURT) + ] + combinations = [{id: players[id] for id in clump} for clump in clumps] + return combinations + + +def create_random_games(courts: CourtList, players: PlayerList): + active_courts, reserved_courts = courts.separate_courts() + + # Block the courts for other activities + reserved_games = _create_placeholders(reserved_courts, "RESERVED") + + # Check if there are too many courts + possible_players = players.separate_players()[0] + possible_games = floor(len(possible_players) / PLAYER_PER_COURT) + + # Create the games + selected_players = select_players( + possible_players, possible_games * PLAYER_PER_COURT + ) + combinations = get_random_combinations(selected_players) + + used_courts = dict(list(active_courts.items())[:possible_games]) + active_games = [ + create_game(court, clump) for court, clump in zip(used_courts, combinations) + ] + + # Create dummy games if there are empty courts + dummy_games = [] + if possible_games < len(active_courts): + unused_courts = dict(list(active_courts.items())[possible_games:]) + dummy_games = _create_placeholders(unused_courts, "---") + + # Return the list of games + games = active_games + dummy_games + reserved_games + games.sort(key=attrgetter("court")) + games = {idx: value for idx, value in enumerate(games)} + return games + + +@dataclass +class Session: + courts: CourtList + players: PlayerList + round: int = 0 + games: dict = field(default_factory=dict) + proposal: dict = field(default_factory=dict) + history: list[dict] = field(default_factory=list) + + def __post_init__(self): + self.history = [] + active_courts, inactive_courts = self.courts.separate_courts() + dummy_games = _create_placeholders(active_courts, "---") + reserved_games = _create_placeholders(inactive_courts, "RESERVED") + games = dummy_games + reserved_games + games.sort(key=attrgetter("court")) + games = {idx: value for idx, value in enumerate(games)} + self.games = games + + def clear_round(self): + self.proposal = {} + return self.games + + def propose_round(self, levels: int = 10): + self.proposal = create_random_games(self.courts, self.players) + return self.proposal + + def confirm_round(self): + self.games = self.proposal + self.history.append(self.proposal) + self.proposal = {} + self.round += 1 + return self.games @@ -1,89 +1,20 @@ -from math import floor from csv import reader +from typing import Tuple from pathlib import Path -from random import sample -from operator import attrgetter from dataclasses import dataclass -from typing import Tuple - - -@dataclass -class CourtList: - total: int - - def __post_init__(self) -> None: - self.courts = {i + 1: True for i in range(self.total)} - - def get_court_status(self, court_number: int) -> bool: - return self.courts[court_number] - - def toggle_court_status(self, court_number: int) -> None: - current_status = self.courts[court_number] - self.courts[court_number] = not current_status - return not current_status - - def separate_courts(self) -> Tuple[dict, dict]: - active_courts = {k: v for k, v in self.courts.items() if v} - inactive_courts = {k: v for k, v in self.courts.items() if not v} - return active_courts, inactive_courts @dataclass class Player: name: str skill: int = 0 - status: bool = True - - -def create_player_from_row(row: list[str]) -> Player: - return Player(name=row[0], skill=row[1], status=(row[2] == "True")) - - -class PlayerList: - - def __init__(self, location: Path = "players.csv"): - with open(location, newline="") as file: - data_reader = reader(file) - next(data_reader, None) - players = [create_player_from_row(row) for row in data_reader] - players.sort(key=attrgetter("name")) - self.players = players - - def get_player(self, player_request: str): - players = [player for player in self.players if player.name == player_request] - players.sort(key=attrgetter("name")) - return players - - def get_active_players(self): - players = [player for player in self.players if player.status] - players.sort(key=attrgetter("name")) - return players - - def toggle_player(self, player_request: Player): - self.players = [ - ( - Player( - name=player.name, - skill=player.skill, - status=not player.status, - ) - if player.name == player_request - else player - ) - for player in self.players - ] - - def __str__(self): - display_string = "" - for player in self.players: - display_string += player.__str__() + "\n" - return display_string + status: bool = False @dataclass class Team: - player1: Player - player2: Player + player1: str + player2: str @dataclass @@ -93,85 +24,53 @@ class Game: team2: Team -DUMMY_PLAYER = Player(name="-") -DUMMY_TEAM = Team(player1=DUMMY_PLAYER, player2=DUMMY_PLAYER) - - -def create_dummy_games(courts: dict): - return [Game(court, DUMMY_TEAM, DUMMY_TEAM) for court, _ in courts.items()] - - -RESERVED_PLAYER = Player(name="Reserved") -RESERVED_TEAM = Team(player1=RESERVED_PLAYER, player2=RESERVED_PLAYER) - - -def create_reserved_games(courts: dict): - return [Game(court, RESERVED_TEAM, RESERVED_TEAM) for court, _ in courts.items()] - +@dataclass +class CourtList: + total: int -PLAYER_PER_COURT = 4 + def __post_init__(self) -> None: + self.courts = {i + 1: True for i in range(self.total)} + def get_court_status(self, court_number: int) -> bool: + return self.courts[court_number] -def create_game(court: int, players: list[Player]): - team1 = Team(player1=players[0], player2=players[3]) - team2 = Team(player1=players[1], player2=players[2]) - return Game(court, team1, team2) + def toggle_court_status(self, court_number: int) -> None: + new_status = not self.courts[court_number] + self.courts[court_number] = new_status + return new_status + def get_courts(self) -> dict: + return self.courts -def create_possible_games(courts: dict, players: list[Player]): - count = len(players) - clumped_players = [ - players[i : i + PLAYER_PER_COURT] for i in range(0, count, PLAYER_PER_COURT) - ] - games = [create_game(court, clump) for court, clump in zip(courts, clumped_players)] - return games + def separate_courts(self) -> Tuple[dict, dict]: + active_courts = {k: v for k, v in self.courts.items() if v} + inactive_courts = {k: v for k, v in self.courts.items() if not v} + return active_courts, inactive_courts @dataclass -class GameList: +class PlayerList: + location: Path + + def __post_init__(self) -> None: + with open(self.location, newline="") as file: + pointer = reader(file) + next(pointer, None) + players = {id: Player(row[0], row[1]) for id, row in enumerate(pointer)} + self.players = players - def clear(self, court_list: CourtList): - active_courts, inactive_courts = court_list.separate_courts() - dummy_games = create_dummy_games(active_courts) - reserved_games = create_reserved_games(inactive_courts) - games = dummy_games + reserved_games - games.sort(key=attrgetter("court")) - self.games = games + def get_player_status(self, player_number: int) -> bool: + return self.players[player_number].status - def create_random_games(self, courts: CourtList, player_list: PlayerList): - active_players = player_list.get_active_players() - active_courts, inactive_courts = courts.separate_courts() - possible_games = floor(len(active_players) / PLAYER_PER_COURT) - if possible_games > len(active_courts): - possible_games = len(active_courts) - current_players = sample(active_players, possible_games * PLAYER_PER_COURT) - active_games = create_possible_games( - dict(list(active_courts.items())[:possible_games]), current_players - ) - dummy_games = create_dummy_games( - dict(list(active_courts.items())[possible_games:]) - ) - reserved_games = create_reserved_games(inactive_courts) - games = active_games + dummy_games + reserved_games - games.sort(key=attrgetter("court")) - self.games = games + 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 create_skilled_games(self, courts: CourtList, player_list: PlayerList): - active_players = player_list.get_active_players() - active_courts, inactive_courts = courts.separate_courts() - possible_games = floor(len(active_players) / PLAYER_PER_COURT) - if possible_games > len(active_courts): - possible_games = len(active_courts) - current_players = sample(active_players, possible_games * PLAYER_PER_COURT) - current_players.sort(key=attrgetter("skill")) + def get_players(self) -> dict: + return self.players - active_games = create_possible_games( - dict(list(active_courts.items())[:possible_games]), current_players - ) - dummy_games = create_dummy_games( - dict(list(active_courts.items())[possible_games:]) - ) - reserved_games = create_reserved_games(inactive_courts) - games = active_games + dummy_games + reserved_games - games.sort(key=attrgetter("court")) - self.games = games + def separate_players(self) -> Tuple[dict, dict]: + active_players = {k: v for k, v in self.players.items() if v.status} + inactive_players = {k: v for k, v in self.players.items() if not v.status} + return active_players, inactive_players diff --git a/templates/courts.j2 b/templates/courts.j2 index 4e3cb3a..5dc64f9 100644 --- a/templates/courts.j2 +++ b/templates/courts.j2 @@ -2,7 +2,10 @@ <div class="grid"> {% for key, value in courts.items() %} <div class="cell"> - <button class={% if value %} "button is-success" {% else %} "button is-danger" {% endif %} hx-post="{{ "/court-toggle/" ~ key}}" hx-trigger="click" hx-swap="outerHTML">{{ key }}</button> + <button class={% if value %} "button is-success" {% else %} "button is-danger" {% endif %} + hx-post="{{ "/court-toggle/" ~ key}}" + hx-trigger="click" + hx-swap="outerHTML">{{ key }}</button> </div> {% endfor %} </div> diff --git a/templates/games.j2 b/templates/games.j2 index b070b16..55cabf4 100644 --- a/templates/games.j2 +++ b/templates/games.j2 @@ -1,17 +1,20 @@ <table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth" id="games"> <thead> + <th colspan="5" align="center"> {{ title }} </th> +</thead> +<thead> <th> Court </th> - <th colspan="2"> Team 1 </th> - <th colspan="2"> Team 2 </th> + <th colspan="2" align="center"> Team 1 </th> + <th colspan="2" align="center"> Team 2 </th> </thead> <tbody> -{% for game in games %} +{% for _, game in games.items() %} <tr> - <th>{{ game.court.number }}</th> - <td>{{ game.team1.player1.name }}</th> - <td>{{ game.team1.player2.name }}</th> - <td>{{ game.team2.player1.name }}</th> - <td>{{ game.team2.player2.name }}</th> + <th>{{ game.court }}</th> + <td>{{ game.team1.player1 }}</th> + <td>{{ game.team1.player2 }}</th> + <td>{{ game.team2.player1 }}</th> + <td>{{ game.team2.player2 }}</th> </tr> {% endfor %} </tbody> diff --git a/templates/index.j2 b/templates/index.j2 index cfd32b5..3a2118b 100644 --- a/templates/index.j2 +++ b/templates/index.j2 @@ -1,52 +1,67 @@ <!doctype html> <html lang="en"> - <head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1"> - <title>🏸Match Up!</title> - <link rel="icon" type="image/x-icon" href="/images/favicon.ico"> - <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.css"> - <script src="https://unpkg.com/htmx.org@2.0.0/dist/htmx.min.js"></script> - <style> - button { - width: 100%; - } - td { - text-align:center; - width:20%; - } - </style> - </head> - <body> - <section class="hero"> - <div class="hero-body"> - <p class="title">🏸The Smashing Fellows </p> - </div> - </section> - <section class="section"> - <p class="title">Games</p> - <div class="columns"> - <div class="column"></div> - <div class="column"><button class="button is-success" hx-post="/random-games" hx-trigger="click" hx-target="#games">Random Games</button></div> - <div class="column"><button class="button is-success" hx-post="/skilled-games" hx-trigger="click" hx-target="#games">Skilled Games</button></div> - <div class="column"></div> - <div class="column"><button class="button is-danger" hx-post="/clear-games" hx-trigger="click" hx-target="#games">Clear</button></div> - <div class="column"></div> - </div> - <div hx-get="/game-list" hx-swap="outerHTML" hx-trigger="revealed"></div> - </section> - <section class="section"> - <h2 class="title">Courts</h2> - <div hx-get="/court-list" hx-swap="outerHTML" hx-trigger="revealed"></div> - </section> - <section class="section"> - <h2 class="title">Players</h2> - <div hx-get="/player-list" hx-swap="outerHTML" hx-trigger="revealed"></div> - </section> - <footer class="footer"> - <div class="content has-text-centered"> - Made by <a href="https://karanjayachandra.com/">K. Jayachandra</a> using <a href="https://flask.palletsprojects.com">Flask</a>, <a href="https://htmx.org/">HTMX</a> and <a href="https://bulma.io">Bulma</a> - </div> - </footer> - </body> -</html>
\ No newline at end of file + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title>🏸Match Up!</title> + <link rel="icon" type="image/x-icon" href="/images/favicon.ico"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.css"> + <script src="https://unpkg.com/htmx.org@2.0.0/dist/htmx.min.js"></script> + <style> + button, input, .select, .select select { + width: 100%; + } + td { + width: 20%; + } + </style> + </head> + <body> + <section class="hero"> + <div class="hero-body"> + <p class="title">🏸The Smashing Fellows </p> + </div> + </section> + <section class="section"> + <p class="title">Games</p> + <div class="columns"> + <div class="column"> + <div class="select"> + <select name="type"> + <option>10 Levels</option> + <option>8 Levels</option> + <option>6 Levels</option> + <option>4 Levels</option> + <option>2 Levels</option> + <option>0 Levels</option> + </select> + </div> + </div> + <div class="column"> + <button class="button is-success" hx-post="/propose" hx-trigger="click" hx-target="#games" hx-include="[name='type']">Propose</button> + </div> + <div class="column"></div> + <div class="column"> + <button class="button is-success" hx-post="/confirm" hx-trigger="click" hx-target="#games">Confirm</button> + </div> + <div class="column"> + <button class="button is-danger" hx-post="/clear" hx-trigger="click" hx-target="#games">Clear</button> + </div> + </div> + <div hx-post="/clear" hx-swap="outerHTML" hx-trigger="revealed"></div> + </section> + <section class="section"> + <h2 class="title">Courts</h2> + <div hx-get="/courts" hx-swap="outerHTML" hx-trigger="revealed"></div> + </section> + <section class="section"> + <h2 class="title">Players</h2> + <div hx-get="/players" hx-swap="outerHTML" hx-trigger="revealed"></div> + </section> + <footer class="footer"> + <div class="content has-text-centered"> + Made by <a href="https://karanjayachandra.com/">K. Jayachandra</a> using <a href="https://flask.palletsprojects.com">Flask</a>, <a href="https://htmx.org/">HTMX</a> and <a href="https://bulma.io">Bulma</a> + </div> + </footer> + </body> +</html> diff --git a/templates/player.j2 b/templates/player.j2 index 5b70df6..68bea2a 100644 --- a/templates/player.j2 +++ b/templates/player.j2 @@ -1 +1,6 @@ -<button class={% if player.status %} "button is-success" {% else %} "button is-danger" {% endif %} hx-post="{{ "/player-toggle/" ~ player.name}}" hx-trigger="click" hx-swap="outerHTML">{{ player.name }}</button>
\ No newline at end of file +<button class={% if status %} "button is-success" {% else %} "button is-danger" {% endif %} + hx-post="{{ "/player-toggle/" ~ id}}" + hx-trigger="click" + hx-swap="outerHTML"> + {{ name }} +</button>
\ No newline at end of file diff --git a/templates/players.j2 b/templates/players.j2 index 6179dbb..7a13672 100644 --- a/templates/players.j2 +++ b/templates/players.j2 @@ -1,8 +1,8 @@ <div class="fixed-grid has-5-cols"> <div class="grid"> -{% for player in players %} +{% for id, player in players.items() %} <div class="cell"> - <button class={% if player.status %} "button is-success" {% else %} "button is-danger" {% endif %} hx-post="{{ "/player-toggle/" ~ player.name}}" hx-trigger="click" hx-swap="outerHTML">{{ player.name }}</button> + <button class={% if player.status %} "button is-success" {% else %} "button is-danger" {% endif %} hx-post="{{ "/player-toggle/" ~ id}}" hx-trigger="click" hx-swap="outerHTML">{{ player.name }}</button> </div> {% endfor %} </div>
\ No newline at end of file |
