diff --git a/css/leaderboards.css b/css/leaderboards.css index 8f3539d..4453efe 100644 --- a/css/leaderboards.css +++ b/css/leaderboards.css @@ -85,3 +85,7 @@ body { .history-bullets ul { flex-direction: column; } + +table, th, td { + border: 1px solid black; +} diff --git a/install/database_schema.sql b/install/database_schema.sql index 7b1d2fe..a768648 100644 --- a/install/database_schema.sql +++ b/install/database_schema.sql @@ -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) ); diff --git a/src/endgames.py b/src/endgames.py index c775778..3c35faf 100644 --- a/src/endgames.py +++ b/src/endgames.py @@ -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. diff --git a/src/render_site.py b/src/render_site.py index 2f3127e..1361108 100644 --- a/src/render_site.py +++ b/src/render_site.py @@ -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() \ No newline at end of file + render_all() diff --git a/templates/game.html b/templates/game.html new file mode 100644 index 0000000..46eb338 --- /dev/null +++ b/templates/game.html @@ -0,0 +1,55 @@ +{% extends "layout.html" %} + +{% block navbar %} + +{% endblock %} + +{% block content %} +
+
+
+

+ Endgame Statistics for game {{game_id}} +

+ + + + + + + + {% for (turn, best_action, other_actions) in data %} + + + + + + + {% for action in other_actions %} + + + + + + {% endfor %} + {% endfor %} +
TurnActionFractional ProbabilityProbability
{{ turn }}{{ best_action.description }}{{ best_action.enumerator }}/{{ best_action.denominator }}{{ best_action.win_rate }}%
{{ action.description }}{{ action.enumerator }}/{{ action.denominator }}{{ action.win_rate }}%
+
+
+
+ +{% endblock %}