summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaran Jayachandra <karan.jayachandra@nxp.com>2024-10-16 11:08:15 +0200
committerKaran Jayachandra <karan.jayachandra@nxp.com>2024-10-16 11:08:15 +0200
commitd304a470e5a9fae6fc0c8975f050cb7aa290f74a (patch)
tree2918384d92c63e6bbe3d3fa642317fa1ab96ac07
parent600b6afff3066d5fc9c4eca659f969322e670a77 (diff)
First working version with history
-rw-r--r--app.py21
-rw-r--r--controller.py157
-rw-r--r--model.py19
-rw-r--r--templates/court.j22
-rw-r--r--templates/courts.j22
-rw-r--r--templates/games.j26
-rw-r--r--templates/index.j215
-rw-r--r--templates/player.j22
-rw-r--r--templates/players.j22
-rw-r--r--utilities.py23
10 files changed, 149 insertions, 100 deletions
diff --git a/app.py b/app.py
index 85dd4f0..87151fe 100644
--- a/app.py
+++ b/app.py
@@ -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
diff --git a/model.py b/model.py
index c0b9dd3..3e76085 100644
--- a/model.py
+++ b/model.py
@@ -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