forked from Hanabi/hanabi-league
Render endgame statistics to website
This commit is contained in:
parent
60aa757ba0
commit
1f8d0b867c
5 changed files with 155 additions and 5 deletions
|
@ -85,3 +85,7 @@ body {
|
|||
.history-bullets ul {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
table, th, td {
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
|
|
@ -379,8 +379,8 @@ CREATE TABLE endgames (
|
|||
*/
|
||||
suit_index SMALLINT, /* 0 for clue actions */
|
||||
rank SMALLINT, /* 0 for clue actions */
|
||||
enumerator INTEGER NOT NULL CHECK (enumerator >= 0),
|
||||
denominator INTEGER NOT NULL CHECK (denominator > 0),
|
||||
enumerator BIGINT NOT NULL CHECK (enumerator >= 0),
|
||||
denominator BIGINT NOT NULL CHECK (denominator > 0),
|
||||
PRIMARY KEY (game_id, turn, action_type, suit_index, rank)
|
||||
);
|
||||
|
||||
|
|
|
@ -29,6 +29,10 @@ class EndgameAction:
|
|||
enumerator: int
|
||||
denominator: int
|
||||
|
||||
@property
|
||||
def win_rate(self):
|
||||
return self.enumerator / self.denominator
|
||||
|
||||
|
||||
def analyze_and_store_game(game_id: int) -> int:
|
||||
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]:
|
||||
cur = conn_manager.get_new_cursor()
|
||||
cur.execute(
|
||||
"SELECT (turn, action_type, suit_index, rank, enumerator, denominator) "
|
||||
"SELECT turn, action_type, suit_index, rank, enumerator, denominator "
|
||||
"FROM endgames "
|
||||
"WHERE game_id = %s "
|
||||
"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)
|
||||
|
||||
|
||||
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():
|
||||
"""
|
||||
Will continuously query database to analyze endgames.
|
||||
|
|
|
@ -17,11 +17,11 @@ import constants
|
|||
import config
|
||||
import utils
|
||||
from dataclasses import dataclass
|
||||
import endgames
|
||||
|
||||
from database import conn_manager
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlayerEntry:
|
||||
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]
|
||||
|
||||
|
||||
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):
|
||||
rating_lists = get_rating_lists()
|
||||
streak_lists = get_streak_list()
|
||||
|
@ -842,7 +914,8 @@ def render_all():
|
|||
render_main_site(env, out_dir)
|
||||
render_player_pages(env, out_dir)
|
||||
render_variant_pages(env, out_dir)
|
||||
render_game_pages(env, out_dir)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
render_all()
|
||||
render_all()
|
||||
|
|
55
templates/game.html
Normal file
55
templates/game.html
Normal 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 %}
|
Loading…
Reference in a new issue