Compare commits
4 Commits
aa5d55124d
...
d79a6be274
| Author | SHA1 | Date | |
|---|---|---|---|
| d79a6be274 | |||
| d8240aeae0 | |||
| 2df45a15f8 | |||
| c1a13d71f7 |
@@ -69,7 +69,9 @@ def print_score_table(wordle_day, scores):
|
|||||||
print_missing_names(wordle_day, scoreless_names)
|
print_missing_names(wordle_day, scoreless_names)
|
||||||
|
|
||||||
|
|
||||||
def _save_db_scores(wordle_day: wordlinator.utils.WordleDay, scores: dict):
|
def _save_db_scores(
|
||||||
|
wordle_day: wordlinator.utils.WordleDay, scores: dict, twitter_scores
|
||||||
|
):
|
||||||
db = wordlinator.db.pg.WordleDb()
|
db = wordlinator.db.pg.WordleDb()
|
||||||
hole_data = wordle_day.golf_hole
|
hole_data = wordle_day.golf_hole
|
||||||
if not hole_data:
|
if not hole_data:
|
||||||
@@ -145,7 +147,7 @@ async def main_update(
|
|||||||
updated_scores = sheets_client.update_scores(today_scores)
|
updated_scores = sheets_client.update_scores(today_scores)
|
||||||
|
|
||||||
rich.print("[green]Saving scores in db...")
|
rich.print("[green]Saving scores in db...")
|
||||||
_save_db_scores(wordle_day, updated_scores)
|
_save_db_scores(wordle_day, updated_scores, today_scores)
|
||||||
|
|
||||||
print_score_table(wordle_day, today_scores)
|
print_score_table(wordle_day, today_scores)
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ class Score(BaseModel):
|
|||||||
user_id = peewee.ForeignKeyField(User, "user_id", null=False)
|
user_id = peewee.ForeignKeyField(User, "user_id", null=False)
|
||||||
game_id = peewee.ForeignKeyField(Game, "game_id", null=False)
|
game_id = peewee.ForeignKeyField(Game, "game_id", null=False)
|
||||||
hole_id = peewee.ForeignKeyField(Hole, "hole_id", null=False)
|
hole_id = peewee.ForeignKeyField(Hole, "hole_id", null=False)
|
||||||
|
tweet_id = peewee.CharField(max_length=255, null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
primary_key = peewee.CompositeKey("user_id", "game_id", "hole_id")
|
primary_key = peewee.CompositeKey("user_id", "game_id", "hole_id")
|
||||||
@@ -127,8 +128,17 @@ class WordleDb:
|
|||||||
def get_scores(self, round_no):
|
def get_scores(self, round_no):
|
||||||
round = self.get_or_create_round(round_no)
|
round = self.get_or_create_round(round_no)
|
||||||
res = (
|
res = (
|
||||||
Score.select(Score, Player.game_id)
|
Score.select(
|
||||||
|
Score,
|
||||||
|
Hole.hole,
|
||||||
|
User.username,
|
||||||
|
Player.game_id,
|
||||||
|
)
|
||||||
.join(Player, on=(Score.user_id == Player.user_id))
|
.join(Player, on=(Score.user_id == Player.user_id))
|
||||||
|
.switch(Score)
|
||||||
|
.join(Hole, on=(Score.hole_id == Hole.hole_id))
|
||||||
|
.switch(Score)
|
||||||
|
.join(User, on=(Score.user_id == User.user_id))
|
||||||
.filter(Player.game_id == round.game_id)
|
.filter(Player.game_id == round.game_id)
|
||||||
.filter(Score.game_id == round.game_id)
|
.filter(Score.game_id == round.game_id)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ class WordleTweet:
|
|||||||
wordle_day: wordlinator.utils.WordleDay
|
wordle_day: wordlinator.utils.WordleDay
|
||||||
raw_score: int
|
raw_score: int
|
||||||
user: TwitterUser
|
user: TwitterUser
|
||||||
|
tweet_id: str
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def score(self):
|
def score(self):
|
||||||
@@ -86,6 +87,7 @@ class WordleTweet:
|
|||||||
return cls(
|
return cls(
|
||||||
created_at=created,
|
created_at=created,
|
||||||
text=tweet["text"],
|
text=tweet["text"],
|
||||||
|
tweet_id=tweet["id"],
|
||||||
wordle_day=wordlinator.utils.WordleDay.from_wordle_no(wordle_no),
|
wordle_day=wordlinator.utils.WordleDay.from_wordle_no(wordle_no),
|
||||||
raw_score=score,
|
raw_score=score,
|
||||||
user=twitter_user,
|
user=twitter_user,
|
||||||
|
|||||||
@@ -1,23 +1,142 @@
|
|||||||
import collections
|
import collections
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
|
import wordlinator.db.pg
|
||||||
|
|
||||||
def golf_score(score_list: typing.List) -> int:
|
############
|
||||||
scores = [s.score for s in score_list]
|
# Mappings #
|
||||||
score_count = len(scores)
|
############
|
||||||
score = sum(scores) - (score_count * 4)
|
|
||||||
return score
|
SCORE_NAME_MAP = {
|
||||||
|
1: "Hole-in-1",
|
||||||
|
2: "Eagle",
|
||||||
|
3: "Birdie",
|
||||||
|
4: "Par",
|
||||||
|
5: "Bogey",
|
||||||
|
6: "Double Bogey",
|
||||||
|
7: "Fail",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_user_scorelist(
|
###############
|
||||||
username: str, scores: typing.List
|
# ScoreMatrix #
|
||||||
) -> typing.Dict[str, typing.Any]:
|
###############
|
||||||
scores = list(sorted(scores, key=lambda s: s.hole_id.hole))
|
|
||||||
return {
|
T = typing.TypeVar("T", bound="ScoreContainer")
|
||||||
"Name": username,
|
|
||||||
"Score": golf_score(scores),
|
|
||||||
**{f"Hole {s.hole_id.hole}": s.score for s in scores},
|
class ScoreContainer:
|
||||||
}
|
def __init__(self, scores: typing.List[wordlinator.db.pg.Score]):
|
||||||
|
self._scores = scores
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_attribute(
|
||||||
|
score: wordlinator.db.pg.Score, attribute_path: typing.List[str]
|
||||||
|
):
|
||||||
|
attribute = score
|
||||||
|
for path_part in attribute_path:
|
||||||
|
attribute = getattr(attribute, path_part)
|
||||||
|
return attribute
|
||||||
|
|
||||||
|
def dict_by(
|
||||||
|
self, attribute_path: str, container_class: typing.Type[T]
|
||||||
|
) -> typing.Dict[typing.Any, T]:
|
||||||
|
data_dict = collections.defaultdict(list)
|
||||||
|
path_parts = attribute_path.split(".")
|
||||||
|
|
||||||
|
for score in self._scores:
|
||||||
|
data_dict[self._get_attribute(score, path_parts)].append(score)
|
||||||
|
|
||||||
|
return {k: container_class(v) for k, v in data_dict.items()}
|
||||||
|
|
||||||
|
|
||||||
|
class ScoreRow(ScoreContainer):
|
||||||
|
@property
|
||||||
|
def total(self) -> int:
|
||||||
|
return sum(s.score for s in self._scores)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def count(self) -> int:
|
||||||
|
return len(self._scores)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def average(self) -> float:
|
||||||
|
return round(self.total / len(self._scores), 2)
|
||||||
|
|
||||||
|
|
||||||
|
class UserRow(ScoreRow):
|
||||||
|
def __init__(self, scores, username=None):
|
||||||
|
super().__init__(scores)
|
||||||
|
self.username = username or scores[0].user_id.username
|
||||||
|
|
||||||
|
@property
|
||||||
|
def golf_score(self) -> int:
|
||||||
|
return self.total - (self.count * 4)
|
||||||
|
|
||||||
|
def sorted_scores(self):
|
||||||
|
yield from sorted(self._scores, key=lambda s: s.hole_id.hole)
|
||||||
|
|
||||||
|
def raw_values(self):
|
||||||
|
yield from (s.score for s in self.sorted_scores())
|
||||||
|
|
||||||
|
def _present_format(self, score):
|
||||||
|
if score.tweet_id:
|
||||||
|
return (
|
||||||
|
f"[{score.score}]"
|
||||||
|
f"(https://twitter.com/{self.username}/status/{score.tweet_id})"
|
||||||
|
)
|
||||||
|
return score.score
|
||||||
|
|
||||||
|
def presentation_values(self, hole_no=None):
|
||||||
|
res = {s.hole_id.hole: self._present_format(s) for s in self.sorted_scores()}
|
||||||
|
if hole_no:
|
||||||
|
for i in range(1, hole_no + 1):
|
||||||
|
if i not in res:
|
||||||
|
res[i] = ""
|
||||||
|
return res
|
||||||
|
|
||||||
|
def user_row(self, hole_no=None):
|
||||||
|
return {
|
||||||
|
"Name": self.username,
|
||||||
|
"Score": self.golf_score,
|
||||||
|
**self.presentation_values(hole_no=hole_no),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ScoreMatrix(ScoreContainer):
|
||||||
|
def by_user(self):
|
||||||
|
return self.dict_by("user_id.username", UserRow)
|
||||||
|
|
||||||
|
def for_user(self, username):
|
||||||
|
user_scores = [s for s in self._scores if s.user_id.username == username]
|
||||||
|
return UserRow(scores=user_scores, username=username)
|
||||||
|
|
||||||
|
def by_hole(self):
|
||||||
|
return self.dict_by("hole_id.hole", ScoreRow)
|
||||||
|
|
||||||
|
def for_hole(self, hole_no):
|
||||||
|
hole_scores = [s for s in self._scores if s.hole_id.hole == hole_no]
|
||||||
|
return ScoreRow(hole_scores)
|
||||||
|
|
||||||
|
def _level_counts(self, level_scores: "ScoreMatrix"):
|
||||||
|
hole_dict = level_scores.by_hole()
|
||||||
|
return {k: v.count for k, v in sorted(hole_dict.items())}
|
||||||
|
|
||||||
|
def score_breakdown(self):
|
||||||
|
by_score_dict = self.dict_by("score", ScoreMatrix)
|
||||||
|
return {
|
||||||
|
SCORE_NAME_MAP[k]: self._level_counts(v)
|
||||||
|
for k, v in sorted(by_score_dict.items())
|
||||||
|
}
|
||||||
|
|
||||||
|
def user_rows(self, wordle_day):
|
||||||
|
hole_no = wordle_day.golf_hole.hole_no
|
||||||
|
return [u.user_row(hole_no=hole_no) for u in self.by_user().values()]
|
||||||
|
|
||||||
|
|
||||||
|
######################
|
||||||
|
# Formatting Helpers #
|
||||||
|
######################
|
||||||
|
|
||||||
|
|
||||||
def format_string(col, condition):
|
def format_string(col, condition):
|
||||||
@@ -60,17 +179,13 @@ def column_formats(col, pct):
|
|||||||
},
|
},
|
||||||
"backgroundColor": "white",
|
"backgroundColor": "white",
|
||||||
},
|
},
|
||||||
]
|
{
|
||||||
|
"if": {
|
||||||
|
"column_id": col["id"],
|
||||||
def table_rows(score_list):
|
"filter_query": format_string(col, "= ''"),
|
||||||
scores_by_user = collections.defaultdict(list)
|
},
|
||||||
for score in score_list:
|
"backgroundColor": "white",
|
||||||
scores_by_user[score.user_id.username].append(score)
|
},
|
||||||
|
|
||||||
return [
|
|
||||||
get_user_scorelist(username, scores)
|
|
||||||
for username, scores in scores_by_user.items()
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import collections
|
|
||||||
import datetime
|
import datetime
|
||||||
import functools
|
import functools
|
||||||
|
import pathlib
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import dash
|
import dash
|
||||||
@@ -19,7 +19,10 @@ import wordlinator.utils.web
|
|||||||
# Setup Functions #
|
# Setup Functions #
|
||||||
###################
|
###################
|
||||||
|
|
||||||
app = dash.Dash(name="WordleGolf", title="#WordleGolf")
|
assets_dir = pathlib.Path(__file__).parent / "assets"
|
||||||
|
app = dash.Dash(
|
||||||
|
name="WordleGolf", title="#WordleGolf", assets_folder=str(assets_dir.resolve())
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_ttl_hash(seconds=600):
|
def get_ttl_hash(seconds=600):
|
||||||
@@ -56,7 +59,7 @@ def _scores_from_db(ttl_hash=None):
|
|||||||
|
|
||||||
|
|
||||||
def scores_from_db():
|
def scores_from_db():
|
||||||
return _scores_from_db(get_ttl_hash())
|
return wordlinator.utils.web.ScoreMatrix(_scores_from_db(get_ttl_hash()))
|
||||||
|
|
||||||
|
|
||||||
#################
|
#################
|
||||||
@@ -65,11 +68,11 @@ def scores_from_db():
|
|||||||
|
|
||||||
|
|
||||||
def get_scores():
|
def get_scores():
|
||||||
score_list = scores_from_db()
|
score_matrix = scores_from_db()
|
||||||
table_rows = wordlinator.utils.web.table_rows(score_list)
|
table_rows = score_matrix.user_rows(wordle_today())
|
||||||
|
|
||||||
hole_columns = [
|
hole_columns = [
|
||||||
{"name": f"Hole {i}", "id": f"Hole {i}", "type": "numeric"}
|
{"name": f"{i}", "id": f"{i}", "type": "text", "presentation": "markdown"}
|
||||||
for i in range(1, wordle_today().golf_hole.hole_no + 1)
|
for i in range(1, wordle_today().golf_hole.hole_no + 1)
|
||||||
]
|
]
|
||||||
columns = [
|
columns = [
|
||||||
@@ -119,58 +122,27 @@ def get_scores():
|
|||||||
# Stats Helpers #
|
# Stats Helpers #
|
||||||
#################
|
#################
|
||||||
|
|
||||||
SCORE_NAME_MAP = {
|
|
||||||
1: "Hole-in-1",
|
|
||||||
2: "Eagle",
|
|
||||||
3: "Birdie",
|
|
||||||
4: "Par",
|
|
||||||
5: "Bogey",
|
|
||||||
6: "Double Bogey",
|
|
||||||
7: "Fail",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
def _get_summary_rows(score_matrix):
|
||||||
|
day_dict = score_matrix.by_hole()
|
||||||
|
|
||||||
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 = {
|
totals = {
|
||||||
"Score": "Total",
|
"Score": "Total",
|
||||||
**{day: len(scores) for day, scores in day_dict.items()},
|
**{day: scores.count for day, scores in day_dict.items()},
|
||||||
}
|
}
|
||||||
|
|
||||||
averages = {
|
averages = {
|
||||||
"Score": "Daily Average",
|
"Score": "Daily Average",
|
||||||
**{
|
**{day: scores.average for day, scores in day_dict.items()},
|
||||||
day: round(sum(scores) / len(scores), 2) for day, scores in day_dict.items()
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [totals, averages]
|
return [totals, averages]
|
||||||
|
|
||||||
|
|
||||||
def _stats_dict():
|
def _stats_dict():
|
||||||
score_list = scores_from_db()
|
score_matrix = scores_from_db()
|
||||||
|
table_rows = [{"Score": k, **v} for k, v in score_matrix.score_breakdown().items()]
|
||||||
scores_by_value = collections.defaultdict(list)
|
table_rows.extend(_get_summary_rows(score_matrix))
|
||||||
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
|
return table_rows
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
4
wordlinator/web/assets/style.css
Normal file
4
wordlinator/web/assets/style.css
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user