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 {
|
.history-bullets ul {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table, th, td {
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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,7 +914,8 @@ 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__":
|
||||||
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