aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaran Jayachandra <karan.jayachandra@nxp.com>2024-10-15 12:07:42 +0200
committerKaran Jayachandra <karan.jayachandra@nxp.com>2024-10-15 12:07:42 +0200
commit600b6afff3066d5fc9c4eca659f969322e670a77 (patch)
tree1bccadf85e486520d747bd1144ae98c7d0ad969b
parentccdf8fcb51a47b79d5a1c0042699b0d60c3c304c (diff)
Cleaned up the code significantly
-rw-r--r--app.py58
-rw-r--r--controller.py102
-rw-r--r--model.py185
-rw-r--r--templates/courts.j25
-rw-r--r--templates/games.j219
-rw-r--r--templates/index.j2115
-rw-r--r--templates/player.j27
-rw-r--r--templates/players.j24
8 files changed, 251 insertions, 244 deletions
diff --git a/app.py b/app.py
index 5c39c0d..85dd4f0 100644
--- a/app.py
+++ b/app.py
@@ -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
diff --git a/model.py b/model.py
index bfaa9d5..c0b9dd3 100644
--- a/model.py
+++ b/model.py
@@ -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