diff options
| author | Karan Jayachandra <karan.jayachandra@nxp.com> | 2024-10-16 11:08:15 +0200 |
|---|---|---|
| committer | Karan Jayachandra <karan.jayachandra@nxp.com> | 2024-10-16 11:08:15 +0200 |
| commit | d304a470e5a9fae6fc0c8975f050cb7aa290f74a (patch) | |
| tree | 2918384d92c63e6bbe3d3fa642317fa1ab96ac07 | |
| parent | 600b6afff3066d5fc9c4eca659f969322e670a77 (diff) | |
First working version with history
| -rw-r--r-- | app.py | 21 | ||||
| -rw-r--r-- | controller.py | 157 | ||||
| -rw-r--r-- | model.py | 19 | ||||
| -rw-r--r-- | templates/court.j2 | 2 | ||||
| -rw-r--r-- | templates/courts.j2 | 2 | ||||
| -rw-r--r-- | templates/games.j2 | 6 | ||||
| -rw-r--r-- | templates/index.j2 | 15 | ||||
| -rw-r--r-- | templates/player.j2 | 2 | ||||
| -rw-r--r-- | templates/players.j2 | 2 | ||||
| -rw-r--r-- | utilities.py | 23 |
10 files changed, 149 insertions, 100 deletions
@@ -1,6 +1,6 @@ from os import getenv -from controller import Session from dotenv import load_dotenv +from controller import RoundGenerator from model import CourtList, PlayerList from flask import Flask, render_template, request @@ -8,7 +8,7 @@ from flask import Flask, render_template, request load_dotenv() -S = Session(courts=CourtList(12), players=PlayerList("test_data_large.csv")) +rg = RoundGenerator(courts=CourtList(12), players=PlayerList("test_data_large.csv")) app = Flask("Match Up! Backend") @@ -23,33 +23,38 @@ def home(): @app.route("/propose", methods=["POST"]) def get_random_games(): levels = int(request.form["type"].split()[0]) - return render_template("games.j2", title="Proposal", games=S.propose_round(levels)) + return render_template("games.j2", title="Proposal", games=rg.propose(levels)) + + +@app.route("/confirm", methods=["POST"]) +def confirm_games(): + return render_template("games.j2", title=f"Round: {rg.round}", games=rg.confirm()) @app.route("/clear", methods=["POST"]) def clear_games(): - return render_template("games.j2", title=f"Round: {S.round}", games=S.clear_round()) + return render_template("games.j2", title=f"Round: {rg.round}", games=rg.clear()) @app.route("/player-toggle/<player_request>", methods=["POST"]) def toggle_player(player_request): id = int(player_request) - status, name = S.players.toggle_player_status(id) + status, name = rg.players.toggle_player_status(id) return render_template("player.j2", id=id, status=status, name=name) @app.route("/players", methods=["GET"]) def get_list_of_players(): - return render_template("players.j2", players=S.players.get_players()) + return render_template("players.j2", players=rg.players.get_players()) @app.route("/court-toggle/<court_number>", methods=["POST"]) def toggle_court(court_number): id = int(court_number) - status = S.courts.toggle_court_status(id) + status = rg.courts.toggle_court_status(id) return render_template("court.j2", id=id, status=status) @app.route("/courts", methods=["GET"]) def get_list_of_courts(): - return render_template("courts.j2", courts=S.courts.get_courts()) + return render_template("courts.j2", courts=rg.courts.get_courts()) diff --git a/controller.py b/controller.py index 45ae2bf..751586e 100644 --- a/controller.py +++ b/controller.py @@ -1,102 +1,109 @@ -from math import floor -from random import sample +from typing import Tuple from operator import attrgetter -from dataclasses import dataclass, field -from model import Team, Game, PlayerList, CourtList +from dataclasses import dataclass +from utilities import sample_list +from model import Team, Game, PlayerList, CourtList, MAX_LEVEL, PLAYER_PER_COURT -PLAYER_PER_COURT = 4 - -def _create_placeholders(courts: dict, name: str): +def _create_placeholders(courts: dict, name: str) -> list[Game]: 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) + games = [Game(court, dummy_team, dummy_team) for court in courts] + return games -def select_players(players: dict, count: int): - identifiers = sample(sorted(players), count) - return {id: players[id] for id in identifiers} +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 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) - ] +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_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)} +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 Session: +class RoundGenerator: 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) + proposal: tuple = tuple() + + def __post_init__(self) -> None: + 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_active_players() + identifiers = [*active_players] + play_count = [self.history[player_id] 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 __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 + 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")) - games = {idx: value for idx, value in enumerate(games)} - self.games = games + return games, selected_players - def clear_round(self): - self.proposal = {} + def clear(self) -> list[Game]: + self.proposal = tuple() return self.games - def propose_round(self, levels: int = 10): - self.proposal = create_random_games(self.courts, self.players) - return self.proposal + def propose(self, levels: int) -> list[Game]: + self.proposal = self._generate_proposal(levels) + return self.proposal[0] - def confirm_round(self): - self.games = self.proposal - self.history.append(self.proposal) - self.proposal = {} + 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: + self.history[player_id] = self.history[player_id] + 1 + self.proposal = tuple() self.round += 1 return self.games @@ -1,8 +1,13 @@ +from math import floor from csv import reader from typing import Tuple from pathlib import Path from dataclasses import dataclass +MIN_LEVEL = 1 +MAX_LEVEL = 10 +PLAYER_PER_COURT = 4 + @dataclass class Player: @@ -56,7 +61,9 @@ class PlayerList: 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)} + players = { + id: Player(row[0], int(row[1])) for id, row in enumerate(pointer) + } self.players = players def get_player_status(self, player_number: int) -> bool: @@ -70,7 +77,11 @@ class PlayerList: def get_players(self) -> dict: return self.players - def separate_players(self) -> Tuple[dict, dict]: + def get_possible_game_count(self) -> int: + active_player_count = len(self.get_active_players()) + possible_games = floor(active_player_count / PLAYER_PER_COURT) + return possible_games + + def get_active_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 + return active_players diff --git a/templates/court.j2 b/templates/court.j2 index 5eeb65a..859c942 100644 --- a/templates/court.j2 +++ b/templates/court.j2 @@ -1 +1 @@ -<button class={% if status %} "button is-success" {% else %} "button is-danger" {% endif %} hx-post="{{ "/court-toggle/" ~ id}}" hx-trigger="click" hx-swap="outerHTML">{{ id }}</button>
\ No newline at end of file +<button class={% if status %} "button is-success is-large" {% else %} "button is-danger is-large" {% endif %} hx-post="{{ "/court-toggle/" ~ id}}" hx-trigger="click" hx-swap="outerHTML">{{ id }}</button>
\ No newline at end of file diff --git a/templates/courts.j2 b/templates/courts.j2 index 5dc64f9..2bc8a95 100644 --- a/templates/courts.j2 +++ b/templates/courts.j2 @@ -2,7 +2,7 @@ <div class="grid"> {% for key, value in courts.items() %} <div class="cell"> - <button class={% if value %} "button is-success" {% else %} "button is-danger" {% endif %} + <button class={% if value %} "button is-success is-large" {% else %} "button is-danger is-large" {% endif %} hx-post="{{ "/court-toggle/" ~ key}}" hx-trigger="click" hx-swap="outerHTML">{{ key }}</button> diff --git a/templates/games.j2 b/templates/games.j2 index 55cabf4..50e1087 100644 --- a/templates/games.j2 +++ b/templates/games.j2 @@ -3,14 +3,14 @@ <th colspan="5" align="center"> {{ title }} </th> </thead> <thead> - <th> Court </th> + <th align="center"> Court </th> <th colspan="2" align="center"> Team 1 </th> <th colspan="2" align="center"> Team 2 </th> </thead> <tbody> -{% for _, game in games.items() %} +{% for game in games %} <tr> - <th>{{ game.court }}</th> + <th align="center">{{ game.court }}</th> <td>{{ game.team1.player1 }}</th> <td>{{ game.team1.player2 }}</th> <td>{{ game.team2.player1 }}</th> diff --git a/templates/index.j2 b/templates/index.j2 index 3a2118b..40d0eb1 100644 --- a/templates/index.j2 +++ b/templates/index.j2 @@ -9,10 +9,13 @@ <script src="https://unpkg.com/htmx.org@2.0.0/dist/htmx.min.js"></script> <style> button, input, .select, .select select { - width: 100%; + width: 100%; } + table { + font-size: 30px; + } td { - width: 20%; + width: 20%; } </style> </head> @@ -26,7 +29,7 @@ <p class="title">Games</p> <div class="columns"> <div class="column"> - <div class="select"> + <div class="select is-large"> <select name="type"> <option>10 Levels</option> <option>8 Levels</option> @@ -38,14 +41,14 @@ </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> + <button class="button is-dark is-large" 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> + <button class="button is-dark is-large" 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> + <button class="button is-dark is-large" hx-post="/clear" hx-trigger="click" hx-target="#games">Clear</button> </div> </div> <div hx-post="/clear" hx-swap="outerHTML" hx-trigger="revealed"></div> diff --git a/templates/player.j2 b/templates/player.j2 index 68bea2a..d0f42c6 100644 --- a/templates/player.j2 +++ b/templates/player.j2 @@ -1,4 +1,4 @@ -<button class={% if status %} "button is-success" {% else %} "button is-danger" {% endif %} +<button class={% if status %} "button is-success is-large" {% else %} "button is-danger is-large" {% endif %} hx-post="{{ "/player-toggle/" ~ id}}" hx-trigger="click" hx-swap="outerHTML"> diff --git a/templates/players.j2 b/templates/players.j2 index 7a13672..550ad68 100644 --- a/templates/players.j2 +++ b/templates/players.j2 @@ -2,7 +2,7 @@ <div class="grid"> {% 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/" ~ id}}" hx-trigger="click" hx-swap="outerHTML">{{ player.name }}</button> + <button class={% if player.status %} "button is-success is-large" {% else %} "button is-danger is-large" {% endif %} hx-post="{{ "/player-toggle/" ~ id}}" hx-trigger="click" hx-swap="outerHTML">{{ player.name }}</button> </div> {% endfor %} </div>
\ No newline at end of file diff --git a/utilities.py b/utilities.py new file mode 100644 index 0000000..1ad24b8 --- /dev/null +++ b/utilities.py @@ -0,0 +1,23 @@ +from typing import Tuple +from random import shuffle + + +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 |
