Render endgame statistics to website

This commit is contained in:
Maximilian Keßler 2024-01-14 00:52:27 +01:00
parent 60aa757ba0
commit 1f8d0b867c
Signed by: max
GPG key ID: BCC5A619923C0BA5
5 changed files with 155 additions and 5 deletions

View file

@ -85,3 +85,7 @@ body {
.history-bullets ul { .history-bullets ul {
flex-direction: column; flex-direction: column;
} }
table, th, td {
border: 1px solid black;
}

View file

@ -379,8 +379,8 @@ CREATE TABLE endgames (
*/ */
suit_index SMALLINT, /* 0 for clue actions */ suit_index SMALLINT, /* 0 for clue actions */
rank SMALLINT, /* 0 for clue actions */ rank SMALLINT, /* 0 for clue actions */
enumerator INTEGER NOT NULL CHECK (enumerator >= 0), enumerator BIGINT NOT NULL CHECK (enumerator >= 0),
denominator INTEGER NOT NULL CHECK (denominator > 0), denominator BIGINT NOT NULL CHECK (denominator > 0),
PRIMARY KEY (game_id, turn, action_type, suit_index, rank) PRIMARY KEY (game_id, turn, action_type, suit_index, rank)
); );

View file

@ -29,6 +29,10 @@ class EndgameAction:
enumerator: int enumerator: int
denominator: int denominator: int
@property
def win_rate(self):
return self.enumerator / self.denominator
def analyze_and_store_game(game_id: int) -> int: def analyze_and_store_game(game_id: int) -> int:
actions, return_code = analyze_game_from_db(game_id) actions, return_code = analyze_game_from_db(game_id)
@ -157,7 +161,7 @@ def store_endgame_actions(game_id: int, endgame_actions: List[EndgameAction], re
def load_endgame_actions(game_id: int) -> List[EndgameAction]: def load_endgame_actions(game_id: int) -> List[EndgameAction]:
cur = conn_manager.get_new_cursor() cur = conn_manager.get_new_cursor()
cur.execute( cur.execute(
"SELECT (turn, action_type, suit_index, rank, enumerator, denominator) " "SELECT turn, action_type, suit_index, rank, enumerator, denominator "
"FROM endgames " "FROM endgames "
"WHERE game_id = %s " "WHERE game_id = %s "
"ORDER BY turn ASC, action_type ASC, suit_index ASC, rank ASC", "ORDER BY turn ASC, action_type ASC, suit_index ASC, rank ASC",
@ -205,6 +209,20 @@ def parse_card(card: str) -> hanabi.hanab_game.DeckCard:
return hanabi.hanab_game.DeckCard(suit, rank) return hanabi.hanab_game.DeckCard(suit, rank)
def print_action_type(action_type: hanabi.hanab_game.ActionType) -> str:
match action_type:
case hanabi.hanab_game.ActionType.Play:
return "Play"
case hanabi.hanab_game.ActionType.Discard:
return "Discard"
case hanabi.hanab_game.ActionType.RankClue:
return "Clue"
case hanabi.hanab_game.ActionType.ColorClue:
return "Clue"
case _:
return "Unknown Action"
def work_thread(): def work_thread():
""" """
Will continuously query database to analyze endgames. Will continuously query database to analyze endgames.

View file

@ -17,11 +17,11 @@ import constants
import config import config
import utils import utils
from dataclasses import dataclass from dataclasses import dataclass
import endgames
from database import conn_manager from database import conn_manager
@dataclass @dataclass
class PlayerEntry: class PlayerEntry:
player_name: str player_name: str
@ -713,6 +713,78 @@ def build_unique_variants(variant_rows: List[VariantRow]):
return [row for row in variant_rows if row.num_players == config.config_manager.get_config().min_player_count] return [row for row in variant_rows if row.num_players == config.config_manager.get_config().min_player_count]
def render_game_pages(env: jinja2.Environment, out_dir: Path):
endgames = get_endgame_page_data()
template = env.get_template("game.html")
for game_id, data in endgames.items():
rendered_template = template.render(
total_games_played=get_total_games(),
total_players=get_num_players(),
latest_run=datetime.datetime.now().isoformat(),
game_id=game_id,
data=data
)
output_file = out_dir / 'game' / str(game_id) / 'index.html'
output_file.parent.mkdir(exist_ok=True, parents=True)
with open(output_file, 'w') as f:
f.write(rendered_template)
def format_endgame_action(endgame_action: endgames.EndgameAction):
return "{} {}: {}/{} ~ {}".format(
endgames.print_action_type(endgame_action.action_type),
endgame_action.card,
endgame_action.enumerator,
endgame_action.denominator,
endgame_action.action_type.value
)
@dataclass
class EndgameActionRow:
description: str
enumerator: int
denominator: int
@property
def win_rate(self):
return round(100 * self.enumerator / self.denominator, 3)
def convert_endgame_action(endgame_action: endgames.EndgameAction) -> EndgameActionRow:
description = "{} {}".format(endgames.print_action_type(endgame_action.action_type), endgame_action.card)
return EndgameActionRow(description, endgame_action.enumerator, endgame_action.denominator)
def get_endgame_page_data():
cur = conn_manager.get_new_cursor()
cur.execute(
"SELECT game_id "
"FROM games "
"LEFT OUTER JOIN endgames_analyzed "
" ON endgames_analyzed.game_id = games.id "
"WHERE termination_reason IS NOT NULL"
)
ret = {}
for (game_id, ) in cur.fetchall():
ret[game_id] = []
actions = endgames.load_endgame_actions(game_id)
while len(actions) > 0:
cur_turn = actions[0].turn
actions_this_turn: List[endgames.EndgameAction] = []
while len(actions) > 0 and actions[0].turn == cur_turn:
action, *actions = actions
actions_this_turn.append(action)
actions_this_turn.sort(key=lambda a: -a.win_rate)
best_action, *other_actions = [convert_endgame_action(a) for a in actions_this_turn]
ret[game_id].append(
(cur_turn, best_action, other_actions)
)
return ret
def render_main_site(env: jinja2.Environment, out_dir: Path): def render_main_site(env: jinja2.Environment, out_dir: Path):
rating_lists = get_rating_lists() rating_lists = get_rating_lists()
streak_lists = get_streak_list() streak_lists = get_streak_list()
@ -842,6 +914,7 @@ def render_all():
render_main_site(env, out_dir) render_main_site(env, out_dir)
render_player_pages(env, out_dir) render_player_pages(env, out_dir)
render_variant_pages(env, out_dir) render_variant_pages(env, out_dir)
render_game_pages(env, out_dir)
if __name__ == "__main__": if __name__ == "__main__":

55
templates/game.html Normal file
View file

@ -0,0 +1,55 @@
{% extends "layout.html" %}
{% block navbar %}
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container">
<a class="navbar-brand" href="/">The Hanabi Pro Hunting League</a><a class="navbar-brand" href="#"><small class="text-muted">- Endgame Statistics for {{game_id}}</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" id="Back" href="/">Back</a>
</li>
</ul>
</div>
</div>
</nav>
{% endblock %}
{% block content %}
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="overview">
<div class="container my-5">
<h3>
Endgame Statistics for game {{game_id}}
</h3>
<table>
<tr>
<th>Turn</th>
<th>Action</th>
<th>Fractional Probability</th>
<th>Probability</th>
</tr>
{% for (turn, best_action, other_actions) in data %}
<tr>
<td rowspan="{{ other_actions|length + 1 }}">{{ turn }}</td>
<td>{{ best_action.description }}</td>
<td>{{ best_action.enumerator }}/{{ best_action.denominator }}</td>
<td>{{ best_action.win_rate }}%</td>
</tr>
{% for action in other_actions %}
<tr>
<td>{{ action.description }}</td>
<td>{{ action.enumerator }}/{{ action.denominator }}</td>
<td>{{ action.win_rate }}%</td>
</tr>
{% endfor %}
{% endfor %}
</table>
</div>
</div>
</div>
{% endblock %}