From 488b38c7f31173f03a4ebf2ae359853ca0f4d6c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Mon, 4 Dec 2023 17:52:18 +0100 Subject: [PATCH] Add stat pages for variants --- css/leaderboards.css | 74 ++++++++++++++++++++ css/style.css | 77 +++++++++++++++++++++ install/default_config.yaml | 2 +- src/render_site.py | 95 +++++++++++++++++++++++--- templates/content.html | 6 +- templates/variant.html | 131 ++++++++++++++++++++++++++++++++++++ 6 files changed, 371 insertions(+), 14 deletions(-) create mode 100644 css/leaderboards.css create mode 100644 css/style.css create mode 100644 templates/variant.html diff --git a/css/leaderboards.css b/css/leaderboards.css new file mode 100644 index 0000000..a935c5e --- /dev/null +++ b/css/leaderboards.css @@ -0,0 +1,74 @@ +.player-name { + font-size: 1.25rem; + font-weight: bold; +} +.alt-name { + font-size: 0.75rem; + color: #777; +} +.score { + font-size: 1.5rem; + font-weight: bold; + color: #17a2b8; /* Bootstrap's teal color */ +} +.table td, .table th { + vertical-align: middle; +} +.card-header { + background-color: #f8f9fa; +} +.card-title { + color: #343a40; /* Bootstrap's dark color */ + font-size: 2.0rem; + font-weight: bold; +} +.alt-name { + font-size: 0.75rem; + color: #777; + margin-top: 0.25rem; /* reduce spacing above alt-name */ +} +.score-large { + font-size: 2rem; + font-weight: bold; + color: #17a2b8; /* Bootstrap's 'info' color */ +} +.btn-link { + color: #007bff; + text-decoration: none; /* Remove the underline */ +} +.btn-link:hover { + color: #0056b3; + text-decoration: none; /* Keep the underline removed when hovering */ +} +.btn-link:focus { + outline: none; + box-shadow: none; +} +body { + display: flex; + flex-direction: column; + min-height: 100vh; +} +.content { + flex: 1; +} +.variant-rating { + /*font-size: 1.2rem;*/ + font-weight: bold; + color: #17a2b8; /* Bootstrap's teal color */ +} +.stat-description { + display: inline-block; + width: 19em; +} +.history-bullets { + display: flex; + flex-direction: row; + flex-wrap: wrap; + width: 100%; + align-items: flex-start; + justify-content: space-around; +} +.history-bullets ul { + flex-direction: column; +} diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..4ee79e4 --- /dev/null +++ b/css/style.css @@ -0,0 +1,77 @@ +@import url("https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"); +body { + background: #f9f9f9; + font-family: "Roboto", sans-serif; +} + +.main-content { + padding-top: 100px; + padding-bottom: 100px; +} + +.leaderboard-card { + background: #fff; + margin-bottom: 30px; + border-radius: 5px; + overflow: hidden; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} +.leaderboard-card.leaderboard-card--first { + transform: scale(1.05); +} +.leaderboard-card.leaderboard-card--first .leaderboard-card__top { + background: linear-gradient(45deg, #7e57c2, #ab47bc); + color: #fff; +} +.leaderboard-card__top { + background: #f9f6ff; + padding: 20px 0 30px 0; +} +.leaderboard-card__body { + padding: 15px; + margin-top: -40px; +} + +img.circle-img { + height: 70px; + width: 70px; + border-radius: 70px; + border: 3px solid #fff; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); +} +img.circle-img.circle-img--small { + height: 55px; + width: 55px; + border-radius: 55px; +} + +.table { + border-spacing: 0 15px; + border-collapse: separate; +} +.table thead tr th, +.table thead tr td, +.table tbody tr th, +.table tbody tr td { + vertical-align: middle; + border: none; +} +.table thead tr th:nth-last-child(1), +.table thead tr td:nth-last-child(1), +.table tbody tr th:nth-last-child(1), +.table tbody tr td:nth-last-child(1) { + text-align: center; +} +.table tbody tr { + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + border-radius: 5px; +} +.table tbody tr td { + background: #fff; +} +.table tbody tr td:nth-child(1) { + border-radius: 5px 0 0 5px; +} +.table tbody tr td:nth-last-child(1) { + border-radius: 0 5px 5px 0; +} diff --git a/install/default_config.yaml b/install/default_config.yaml index 5469c23..f7891d9 100644 --- a/install/default_config.yaml +++ b/install/default_config.yaml @@ -35,7 +35,7 @@ k-factor: num_early_games: 30 high_rating: 1700 -min_suits: 4 +min_suits: 5 max_suits: 6 # Corresponds to game IDs from hanab.live diff --git a/src/render_site.py b/src/render_site.py index 10cba8a..7e17f6c 100644 --- a/src/render_site.py +++ b/src/render_site.py @@ -28,11 +28,43 @@ class Leader: @dataclass class VariantStats: - name: str - num_players: int rating: int games_played: int games_won: int + total_bdr: int + total_crits_lost: int + total_moves: int + + @property + def winrate(self): + if self.games_played == 0: + return 0 + return round(100 * float(self.games_won) / self.games_played, 1) + + @property + def average_bdr(self): + if self.games_played == 0: + return 0 + return round(float(self.total_bdr) / self.games_played, 3) + + @property + def average_crits_lost(self): + if self.games_played == 0: + return 0 + return round(float(self.total_crits_lost) / self.games_played, 3) + + @property + def average_moves(self): + if self.games_played == 0: + return 0 + return round(float(self.total_moves) / self.games_played, 3) + +@dataclass +class VariantRow: + variant_id: int + name: str + num_players: int + stats: VariantStats def get_rating_lists() -> Dict[int, List[PlayerEntry]]: @@ -120,15 +152,19 @@ def get_stat_lists(stat_type: str, order_type: str = 'DESC', precision: int = 0, return leaderboard -def get_variant_ratings(): +def get_variant_rows() -> List[VariantRow]: cur = conn_manager.get_new_cursor() cur.execute( "SELECT" + " ratings.id," " ratings.name," " ratings.num_players," " current_rating," " COUNT(games.id) AS games_played," - " COUNT(games.id) FILTER (WHERE ratings.num_suits * 5 = games.score) AS games_won " + " COUNT(games.id) FILTER (WHERE ratings.num_suits * 5 = games.score) AS games_won," + " SUM(game_statistics.num_bottom_deck_risks) AS total_bdr," + " SUM(game_statistics.num_crits_lost) AS total_crits_lost," + " SUM(games.num_turns) AS total_turns " "FROM " " (" " SELECT DISTINCT ON (variants.id, variant_base_ratings.num_players)" @@ -141,19 +177,25 @@ def get_variant_ratings(): " LEFT OUTER JOIN variant_base_ratings" " ON variants.id = variant_base_ratings.variant_id " " LEFT OUTER JOIN variant_ratings " - " ON variant_ratings.variant_id = variant_base_ratings.variant_id AND variant_ratings.num_players = variant_base_ratings.num_players " - " GROUP BY (variants.id, name, variant_base_ratings.num_players, variant_base_ratings.rating, variant_ratings.league_id, variant_ratings.value_after) " + " ON variant_ratings.variant_id = variant_base_ratings.variant_id" + " AND variant_ratings.num_players = variant_base_ratings.num_players " + " GROUP BY (" + " variants.id, name, variant_base_ratings.num_players, variant_base_ratings.rating," + " variant_ratings.league_id, variant_ratings.value_after" + " ) " " ORDER BY variants.id, variant_base_ratings.num_players, league_id DESC" " ) AS ratings " "LEFT OUTER JOIN games " " ON games.variant_id = ratings.id AND games.num_players = ratings.num_players " + "LEFT OUTER JOIN game_statistics" + " ON games.id = game_statistics.game_id " "GROUP BY (ratings.id, ratings.name, ratings.num_players, ratings.current_rating)" "ORDER BY (ratings.id, ratings.num_players)" "" ) return [ - VariantStats(variant_name, num_players, rating, games_played, games_won) - for (variant_name, num_players, rating, games_played, games_won) in cur.fetchall() + VariantRow(variant_id, variant_name, num_players, VariantStats(round(rating), games_played, games_won, total_bdr, total_crits_lost, total_turns)) + for (variant_id, variant_name, num_players, rating, games_played, games_won, total_bdr, total_crits_lost, total_turns) in cur.fetchall() ] @@ -171,11 +213,27 @@ def get_num_players(): return num_users +def build_variant_stats_per_variant(variant_rows: List[VariantRow]): + variant_stats = {} + variant_names = {} + for row in variant_rows: + if row.variant_id not in variant_stats.keys(): + variant_stats[row.variant_id] = {} + if row.variant_id not in variant_names.keys(): + variant_names[row.variant_id] = {} + variant_stats[row.variant_id][row.num_players] = row.stats + variant_names[row.variant_id] = row.name + return variant_stats, variant_names + + def render_leaderboard(): rating_lists = get_rating_lists() streak_lists = get_stat_lists("maximum_streak") leaders = get_leaders(rating_lists, streak_lists) + variant_rows: List[VariantRow] = get_variant_rows() + variant_stats, variant_names = build_variant_stats_per_variant(variant_rows) + leaderboards = { 'Player Rating': rating_lists, 'Maximum Streak': streak_lists, @@ -185,21 +243,38 @@ def render_leaderboard(): } env = jinja2.Environment(loader=jinja2.FileSystemLoader('templates')) + template = env.get_template('content.html') - # rendered_html = template.render(leaders=leaders, leaderboards=leaderboards, variants=variants) rendered_html = template.render( leaders=leaders, leaderboards=leaderboards, total_games_played=get_total_games(), total_players=get_num_players(), latest_run=datetime.datetime.now().isoformat(), - variants=get_variant_ratings() + variants=variant_rows # variants=variants, ) + output_file = Path(constants.WEBSITE_OUTPUT_DIRECTORY) / 'index.html' output_file.parent.mkdir(exist_ok=True, parents=True) with open(output_file, 'w') as f: f.write(rendered_html) + variant_template = env.get_template('variant.html') + for variant_id, stats in variant_stats.items(): + rendered_var = variant_template.render( + total_games_played=get_total_games(), + total_players=get_num_players(), + latest_run=datetime.datetime.now().isoformat(), + variant_stats=stats, + variant_name=variant_names[variant_id] + ) + + output_file = Path(constants.WEBSITE_OUTPUT_DIRECTORY) / 'variant' / str(variant_id) / 'index.html' + output_file.parent.mkdir(exist_ok=True, parents=True) + + with open(output_file, 'w') as f: + f.write(rendered_var) + render_leaderboard() diff --git a/templates/content.html b/templates/content.html index cc5f764..256798f 100644 --- a/templates/content.html +++ b/templates/content.html @@ -69,9 +69,9 @@ {{ variant.name }} {{ variant.num_players }} - {{ variant.rating | int }} - {{ variant.games_played }} - {{ variant.games_won }} + {{ variant.stats.rating | int }} + {{ variant.stats.games_played }} + {{ variant.stats.games_won }} {% endfor %} diff --git a/templates/variant.html b/templates/variant.html new file mode 100644 index 0000000..cb2f0a6 --- /dev/null +++ b/templates/variant.html @@ -0,0 +1,131 @@ + + + + + + Hanabi Pro Hunting League + + + + + + + +
+ + {% for num_players, stats in variant_stats.items() %} +
+
+

+ League statistics for {{variant_name}} ({{num_players}} players) +

+
+
    +
  • + Rating: + {{stats.rating}} +
  • +
  • + Total perfect scores: + {{stats.games_won}} +
  • +
  • + Total bottom deck risk: + {{stats.total_bdr}} +
  • +
  • + Total crits lost: + {{stats.total_crits_lost}} +
  • +
  • + Total turns taken: + {{stats.total_moves}} +
  • +
+
    +
  • + Total games played: + {{stats.games_played}} +
  • +
  • + Winrate: + {{stats.winrate}}% +
  • +
  • + Average bottom deck risk: + {{stats.average_bdr}} +
  • +
  • + Average crits lost: + {{stats.average_crits_lost}} +
  • +
  • + Average turns taken: + {{stats.average_moves}} +
  • +
+
+
+
+ {% endfor %} +
+ + + + + + + + + + + + + + +