341 lines
8.3 KiB
Python
341 lines
8.3 KiB
Python
import collections
|
|
import functools
|
|
import time
|
|
|
|
import dash
|
|
import dash.long_callback
|
|
import diskcache
|
|
import plotly.graph_objs
|
|
|
|
import wordlinator.db.pg as db
|
|
import wordlinator.utils
|
|
|
|
###################
|
|
# Setup Functions #
|
|
###################
|
|
|
|
app = dash.Dash(name="WordleGolf", title="#WordleGolf")
|
|
|
|
|
|
def get_ttl_hash(seconds=600):
|
|
return round(time.time() / seconds)
|
|
|
|
|
|
cache = diskcache.Cache("./cache")
|
|
long_callback_manager = dash.long_callback.DiskcacheLongCallbackManager(
|
|
cache, cache_by=get_ttl_hash
|
|
)
|
|
|
|
|
|
@functools.lru_cache()
|
|
def _wordle_today(ttl_hash=None):
|
|
return wordlinator.utils.get_wordle_today()
|
|
|
|
|
|
def wordle_today():
|
|
return _wordle_today(get_ttl_hash())
|
|
|
|
|
|
@functools.lru_cache()
|
|
def _scores_from_db(ttl_hash=None):
|
|
wordle_day = wordle_today()
|
|
return db.WordleDb().get_scores(wordle_day.golf_hole.game_no)
|
|
|
|
|
|
def scores_from_db():
|
|
return _scores_from_db(get_ttl_hash())
|
|
|
|
|
|
#################
|
|
# Score Helpers #
|
|
#################
|
|
|
|
|
|
def _golf_score(score_list):
|
|
scores = [s.score for s in score_list]
|
|
score_count = len(scores)
|
|
score = sum(scores) - (score_count * 4)
|
|
return score
|
|
|
|
|
|
def _get_user_scorelist(username, scores):
|
|
scores = list(sorted(scores, key=lambda s: s.hole_id.hole))
|
|
return {
|
|
"Name": username,
|
|
"Score": _golf_score(scores),
|
|
**{f"Hole {s.hole_id.hole}": s.score for s in scores},
|
|
}
|
|
|
|
|
|
def _format_string(col, condition):
|
|
return "{" + col["id"] + "}" + f" {condition}"
|
|
|
|
|
|
def _column_formats(col):
|
|
return [
|
|
{
|
|
"if": {
|
|
"column_id": col["id"],
|
|
"filter_query": _format_string(col, "> 4"),
|
|
},
|
|
"backgroundColor": "red",
|
|
},
|
|
{
|
|
"if": {
|
|
"column_id": col["id"],
|
|
"filter_query": _format_string(col, "= 4"),
|
|
},
|
|
"backgroundColor": "orange",
|
|
},
|
|
{
|
|
"if": {
|
|
"column_id": col["id"],
|
|
"filter_query": _format_string(col, "< 4"),
|
|
},
|
|
"backgroundColor": "green",
|
|
},
|
|
{
|
|
"if": {
|
|
"column_id": col["id"],
|
|
"filter_query": _format_string(col, "is nil"),
|
|
},
|
|
"backgroundColor": "white",
|
|
},
|
|
]
|
|
|
|
|
|
def get_scores():
|
|
score_list = scores_from_db()
|
|
scores_by_user = collections.defaultdict(list)
|
|
for score in score_list:
|
|
scores_by_user[score.user_id.username].append(score)
|
|
|
|
table_rows = [
|
|
_get_user_scorelist(username, scores)
|
|
for username, scores in scores_by_user.items()
|
|
]
|
|
|
|
hole_columns = [
|
|
{"name": f"Hole {i}", "id": f"Hole {i}", "type": "numeric"}
|
|
for i in range(1, wordle_today().golf_hole.hole_no + 1)
|
|
]
|
|
columns = [
|
|
{"name": "Name", "id": "Name", "type": "text"},
|
|
{"name": "Score", "id": "Score", "type": "text"},
|
|
*hole_columns,
|
|
]
|
|
|
|
color_formatting = [
|
|
format_entry
|
|
for column_formats in [_column_formats(col) for col in hole_columns]
|
|
for format_entry in column_formats
|
|
]
|
|
formatting = [
|
|
{"if": {"column_id": "Name"}, "textAlign": "center"},
|
|
*color_formatting,
|
|
]
|
|
return dash.dash_table.DataTable(
|
|
table_rows,
|
|
columns,
|
|
style_table={"width": "80%", "margin": "auto"},
|
|
style_cell={"textAlign": "center"},
|
|
style_data={"width": "10%"},
|
|
style_as_list_view=True,
|
|
style_data_conditional=formatting,
|
|
sort_action="native",
|
|
)
|
|
|
|
|
|
#################
|
|
# Stats Helpers #
|
|
#################
|
|
|
|
SCORE_NAME_MAP = {
|
|
1: "Hole-in-1",
|
|
2: "Eagle",
|
|
3: "Birdie",
|
|
4: "Par",
|
|
5: "Bogey",
|
|
6: "Double Bogey",
|
|
7: "Fail",
|
|
}
|
|
|
|
|
|
def _get_score_breakdown(score, holes):
|
|
score_row = {"Score": SCORE_NAME_MAP[score]}
|
|
days = sorted(set(holes))
|
|
for day in days:
|
|
score_row[day] = holes.count(day)
|
|
return score_row
|
|
|
|
|
|
def _get_summary_rows(score_list):
|
|
days = list(sorted(set((score.hole_id.hole for score in score_list))))
|
|
day_dict = {
|
|
day: [score.score for score in score_list if score.hole_id.hole == day]
|
|
for day in days
|
|
}
|
|
totals = {
|
|
"Score": "Total",
|
|
**{day: len(scores) for day, scores in day_dict.items()},
|
|
}
|
|
|
|
averages = {
|
|
"Score": "Daily Average",
|
|
**{
|
|
day: round(sum(scores) / len(scores), 2) for day, scores in day_dict.items()
|
|
},
|
|
}
|
|
|
|
return [totals, averages]
|
|
|
|
|
|
def _stats_dict():
|
|
score_list = scores_from_db()
|
|
|
|
scores_by_value = collections.defaultdict(list)
|
|
for score in score_list:
|
|
scores_by_value[score.score].append(score.hole_id.hole)
|
|
|
|
table_rows = []
|
|
for score in sorted(scores_by_value.keys()):
|
|
table_rows.append(_get_score_breakdown(score, scores_by_value[score]))
|
|
|
|
table_rows.extend(_get_summary_rows(score_list))
|
|
return table_rows
|
|
|
|
|
|
def get_daily_stats():
|
|
table_rows = _stats_dict()
|
|
|
|
columns = [
|
|
{"name": n, "id": n}
|
|
for n in (
|
|
"Score",
|
|
*[f"{i}" for i in range(1, wordle_today().golf_hole.hole_no + 1)],
|
|
)
|
|
]
|
|
return dash.dash_table.DataTable(
|
|
table_rows,
|
|
columns=columns,
|
|
style_as_list_view=True,
|
|
style_data_conditional=[
|
|
{"if": {"filter_query": "{Score} = 'Total'"}, "fontWeight": "bold"},
|
|
{"if": {"filter_query": "{Score} = 'Daily Average'"}, "fontWeight": "bold"},
|
|
],
|
|
style_table={"width": "80%", "margin": "auto"},
|
|
)
|
|
|
|
|
|
#################
|
|
# Graph Helpers #
|
|
#################
|
|
|
|
|
|
SCORE_COLOR_DICT = {
|
|
"Hole-in-1": "black",
|
|
"Eagle": "darkgreen",
|
|
"Birdie": "lightgreen",
|
|
"Par": "white",
|
|
"Bogey": "palevioletred",
|
|
"Double Bogey": "orangered",
|
|
"Fail": "darkred",
|
|
}
|
|
|
|
|
|
def get_line_graph():
|
|
rows = _stats_dict()
|
|
figure = plotly.graph_objs.Figure()
|
|
total = [r for r in rows if r["Score"] == "Total"][0]
|
|
rows = [r for r in rows if r["Score"] not in ("Total", "Daily Average")]
|
|
total.pop("Score")
|
|
for row in rows:
|
|
score = row.pop("Score")
|
|
y_values = []
|
|
for k in row.keys():
|
|
row_val = row.get(k)
|
|
total_val = total.get(k)
|
|
pct = row_val / total_val * 100
|
|
y_values.append(pct)
|
|
figure.add_trace(
|
|
plotly.graph_objs.Scatter(
|
|
x=list(row.keys()),
|
|
y=y_values,
|
|
fill="tonexty",
|
|
name=score,
|
|
line={"color": SCORE_COLOR_DICT[score]},
|
|
stackgroup="dailies",
|
|
)
|
|
)
|
|
figure.update_xaxes(tickvals=list(total.keys()), title_text="Days")
|
|
figure.update_yaxes(title_text="Percent")
|
|
figure.update_layout(yaxis_range=[0, 100])
|
|
return dash.dcc.Graph(figure=figure)
|
|
|
|
|
|
#############
|
|
# App Setup #
|
|
#############
|
|
|
|
app.layout = dash.html.Div(
|
|
children=[
|
|
dash.html.H1("#WordleGolf", style={"textAlign": "center"}, id="title"),
|
|
dash.html.Div(
|
|
[
|
|
dash.html.H2("User Scores", style={"textAlign": "center"}),
|
|
dash.html.Div("Loading...", id="user-scores"),
|
|
]
|
|
),
|
|
dash.html.Div(
|
|
[
|
|
dash.html.H2("Score Graph", style={"textAlign": "center"}),
|
|
dash.html.Div("Loading...", id="stats-graph"),
|
|
]
|
|
),
|
|
dash.html.Div(
|
|
[
|
|
dash.html.H2("Daily Stats", style={"textAlign": "center"}),
|
|
dash.html.Div("Loading...", id="daily-stats"),
|
|
]
|
|
),
|
|
]
|
|
)
|
|
|
|
|
|
@app.long_callback(
|
|
output=dash.dependencies.Output("user-scores", "children"),
|
|
inputs=dash.dependencies.Input("title", "children"),
|
|
manager=long_callback_manager,
|
|
)
|
|
def get_scores_chart(_):
|
|
return get_scores()
|
|
|
|
|
|
@app.long_callback(
|
|
output=dash.dependencies.Output("daily-stats", "children"),
|
|
inputs=dash.dependencies.Input("title", "children"),
|
|
manager=long_callback_manager,
|
|
)
|
|
def get_stats_chart(_):
|
|
return get_daily_stats()
|
|
|
|
|
|
@app.long_callback(
|
|
output=dash.dependencies.Output("stats-graph", "children"),
|
|
inputs=dash.dependencies.Input("title", "children"),
|
|
manager=long_callback_manager,
|
|
)
|
|
def get_stats_graph(_):
|
|
return get_line_graph()
|
|
|
|
|
|
server = app.server
|
|
|
|
|
|
def serve(debug=True):
|
|
app.run_server(debug=debug)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
serve()
|