Add stat pages for variants

This commit is contained in:
Maximilian Keßler 2023-12-04 17:52:18 +01:00
parent f5d3cfdad4
commit 488b38c7f3
Signed by: max
GPG key ID: BCC5A619923C0BA5
6 changed files with 371 additions and 14 deletions

74
css/leaderboards.css Normal file
View file

@ -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;
}

77
css/style.css Normal file
View file

@ -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;
}

View file

@ -35,7 +35,7 @@ k-factor:
num_early_games: 30 num_early_games: 30
high_rating: 1700 high_rating: 1700
min_suits: 4 min_suits: 5
max_suits: 6 max_suits: 6
# Corresponds to game IDs from hanab.live # Corresponds to game IDs from hanab.live

View file

@ -28,11 +28,43 @@ class Leader:
@dataclass @dataclass
class VariantStats: class VariantStats:
name: str
num_players: int
rating: int rating: int
games_played: int games_played: int
games_won: 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]]: 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 return leaderboard
def get_variant_ratings(): def get_variant_rows() -> List[VariantRow]:
cur = conn_manager.get_new_cursor() cur = conn_manager.get_new_cursor()
cur.execute( cur.execute(
"SELECT" "SELECT"
" ratings.id,"
" ratings.name," " ratings.name,"
" ratings.num_players," " ratings.num_players,"
" current_rating," " current_rating,"
" COUNT(games.id) AS games_played," " 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 " "FROM "
" (" " ("
" SELECT DISTINCT ON (variants.id, variant_base_ratings.num_players)" " SELECT DISTINCT ON (variants.id, variant_base_ratings.num_players)"
@ -141,19 +177,25 @@ def get_variant_ratings():
" LEFT OUTER JOIN variant_base_ratings" " LEFT OUTER JOIN variant_base_ratings"
" ON variants.id = variant_base_ratings.variant_id " " ON variants.id = variant_base_ratings.variant_id "
" LEFT OUTER JOIN variant_ratings " " 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 " " ON variant_ratings.variant_id = variant_base_ratings.variant_id"
" GROUP BY (variants.id, name, variant_base_ratings.num_players, variant_base_ratings.rating, variant_ratings.league_id, variant_ratings.value_after) " " 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" " ORDER BY variants.id, variant_base_ratings.num_players, league_id DESC"
" ) AS ratings " " ) AS ratings "
"LEFT OUTER JOIN games " "LEFT OUTER JOIN games "
" ON games.variant_id = ratings.id AND games.num_players = ratings.num_players " " 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)" "GROUP BY (ratings.id, ratings.name, ratings.num_players, ratings.current_rating)"
"ORDER BY (ratings.id, ratings.num_players)" "ORDER BY (ratings.id, ratings.num_players)"
"" ""
) )
return [ return [
VariantStats(variant_name, num_players, rating, games_played, games_won) VariantRow(variant_id, variant_name, num_players, VariantStats(round(rating), games_played, games_won, total_bdr, total_crits_lost, total_turns))
for (variant_name, num_players, rating, games_played, games_won) in cur.fetchall() 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 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(): def render_leaderboard():
rating_lists = get_rating_lists() rating_lists = get_rating_lists()
streak_lists = get_stat_lists("maximum_streak") streak_lists = get_stat_lists("maximum_streak")
leaders = get_leaders(rating_lists, streak_lists) 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 = { leaderboards = {
'Player Rating': rating_lists, 'Player Rating': rating_lists,
'Maximum Streak': streak_lists, 'Maximum Streak': streak_lists,
@ -185,21 +243,38 @@ def render_leaderboard():
} }
env = jinja2.Environment(loader=jinja2.FileSystemLoader('templates')) env = jinja2.Environment(loader=jinja2.FileSystemLoader('templates'))
template = env.get_template('content.html') template = env.get_template('content.html')
# rendered_html = template.render(leaders=leaders, leaderboards=leaderboards, variants=variants)
rendered_html = template.render( rendered_html = template.render(
leaders=leaders, leaders=leaders,
leaderboards=leaderboards, leaderboards=leaderboards,
total_games_played=get_total_games(), total_games_played=get_total_games(),
total_players=get_num_players(), total_players=get_num_players(),
latest_run=datetime.datetime.now().isoformat(), latest_run=datetime.datetime.now().isoformat(),
variants=get_variant_ratings() variants=variant_rows
# variants=variants, # variants=variants,
) )
output_file = Path(constants.WEBSITE_OUTPUT_DIRECTORY) / 'index.html' output_file = Path(constants.WEBSITE_OUTPUT_DIRECTORY) / 'index.html'
output_file.parent.mkdir(exist_ok=True, parents=True) output_file.parent.mkdir(exist_ok=True, parents=True)
with open(output_file, 'w') as f: with open(output_file, 'w') as f:
f.write(rendered_html) 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() render_leaderboard()

View file

@ -69,9 +69,9 @@
<tr> <tr>
<td class="text-center"><strong>{{ variant.name }}</strong></td> <td class="text-center"><strong>{{ variant.name }}</strong></td>
<td class="text-center">{{ variant.num_players }}</td> <td class="text-center">{{ variant.num_players }}</td>
<td class="text-center variant-rating">{{ variant.rating | int }}</td> <td class="text-center variant-rating">{{ variant.stats.rating | int }}</td>
<td class="text-center">{{ variant.games_played }}</td> <td class="text-center">{{ variant.stats.games_played }}</td>
<td class="text-center">{{ variant.games_won }}</td> <td class="text-center">{{ variant.stats.games_won }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

131
templates/variant.html Normal file
View file

@ -0,0 +1,131 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hanabi Pro Hunting League</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css" integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous">
<link rel="stylesheet" href="../../css/leaderboards.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container">
<a class="navbar-brand" href="#">The Hanabi Pro Hunting League<small class="text-muted"> - Variant Statistics for {{variant_name}}</small></a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link active" id="overview-tab" data-toggle="tab" href="#overview">Overview</a>
</li>
{% for num_players in variant_stats.keys() %}
<li class="nav-item">
<a class="nav-link" id="stats-{{num_players}}p-tab" data-toggle="tab" href="#stats-{{num_players}}p">{{num_players}} Players</a>
</li>
{% endfor %}
</ul>
</div>
</div>
</nav>
<div class="tab-content" id="myTabContent">
{% for num_players, stats in variant_stats.items() %}
<div class="tab-pane fade" id="stats-{{num_players}}p">
<div class="container my-5">
<h3>
League statistics for {{variant_name}} ({{num_players}} players)
</h3>
<div class="history-bullets">
<ul>
<li>
<span class="stat-description">Rating:</span>
{{stats.rating}}
</li>
<li>
<span class="stat-description">Total perfect scores:</span>
{{stats.games_won}}
</li>
<li>
<span class="stat-description">Total bottom deck risk:</span>
{{stats.total_bdr}}
</li>
<li>
<span class="stat-description">Total crits lost:</span>
{{stats.total_crits_lost}}
</li>
<li>
<span class="stat-description">Total turns taken:</span>
{{stats.total_moves}}
</li>
</ul>
<ul>
<li>
<span class="stat-description">Total games played:</span>
{{stats.games_played}}
</li>
<li>
<span class="stat-description">Winrate:</span>
{{stats.winrate}}%
</li>
<li>
<span class="stat-description">Average bottom deck risk:</span>
{{stats.average_bdr}}
</li>
<li>
<span class="stat-description">Average crits lost:</span>
{{stats.average_crits_lost}}
</li>
<li>
<span class="stat-description">Average turns taken:</span>
{{stats.average_moves}}
</li>
</ul>
</div>
</div>
</div>
{% endfor %}
</div>
<footer class="footer mt-auto py-3 bg-light">
<div class="container text-center">
<span class="text-muted">{{ total_games_played }} games | {{ total_players }} players | Thanks for playing <3</span><br>
<span class="text-muted">Last updated: <span id="latestRun">{{ latest_run }}</span></span>
</div>
</footer>
<!-- Bootstrap JavaScript dependencies -->
<script src="https://code.jquery.com/jquery-3.7.0.slim.min.js" integrity="sha256-tG5mcZUtJsZvyKAxYLVXrmjKBVLd6VpVccqz/r4ypFE=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-Fy6S3B9q64WdZWQUiU+q4/2Lc9npb8tCaSX9FK7E8HnRr0Jz8D6OP9dO5Vg3Q9ct" crossorigin="anonymous"></script>
<script>
$(document).ready(function() {
$('.nav-link').on('shown.bs.tab', function(e) {
// Remove 'active' class from all nav-items
$('.nav-link').removeClass('active');
// Add 'active' class to the current nav-item
$(this).addClass('active');
});
});
</script>
<!-- Display time of latest run in local time format -->
<script>
document.addEventListener("DOMContentLoaded", function() {
// Get the latest run date in UTC
var latestRunUtc = new Date(document.getElementById("latestRun").textContent);
// Convert it to the local timezone and format it
var latestRunLocal = latestRunUtc.toLocaleString("en-US", { month: "short", day: "numeric", year: "numeric", hour: "2-digit", minute: "2-digit" });
// Set the text content of the latestRun span to the local date
document.getElementById("latestRun").textContent = latestRunLocal;
});
</script>
</body>
</html>