From 0868da6f5cffa87a3e59ef1b76f2d5156d11d729 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sat, 26 Nov 2022 16:07:49 +0100 Subject: [PATCH] init 2022 --- aocrr_bot.py | 188 +++++++++++++++++++++++++++++++++++++++++++++++++ leaderboard.py | 124 ++++++++++++++++++++++++++++++++ main.py | 41 +++++++++++ skel_day.py | 24 +++++++ start_day.py | 51 ++++++++++++++ 5 files changed, 428 insertions(+) create mode 100644 aocrr_bot.py create mode 100644 leaderboard.py create mode 100644 main.py create mode 100644 skel_day.py create mode 100644 start_day.py diff --git a/aocrr_bot.py b/aocrr_bot.py new file mode 100644 index 0000000..66984ee --- /dev/null +++ b/aocrr_bot.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3.9 + +import json +import requests +import sys +from datetime import datetime, timedelta +from time import sleep +from tools.datafiles import JSONFile +from tools.irc import IrcBot, ServerMessage +from tools.tools import human_readable_time_from_delta + +# IRC_SERVER = "irc.eu.libera.chat" +IRC_SERVER = "irc.tu-ilmenau.de" +IRC_PORT = 6667 +# IRC_CHANNEL = "#aocrr" +IRC_CHANNEL = "#hinterzimmer" +IRC_NICK = "aocrr-bot" +IRC_USER = "aocrr-bot" +IRC_REALNAME = "#aocrr Leaderboard Announcer" + + +def fetch_leaderboard(year: int = datetime.now().year) -> dict: + return json.loads( + requests.get( + "https://adventofcode.com/%d/leaderboard/private/view/711147.json" % year, + cookies={'session': session_id} + ).content + ) + + +def command_info(msg_from: str, message: str): + ircbot.privmsg( + msg_from, + "I am %s => %s" % (ircbot.getUser().nickname, ircbot.getUser().user) + ) + ircbot.privmsg(msg_from, "I am currently in the following channels:") + for c in ircbot.getChannelList(): + ircbot.privmsg(msg_from, "%s => %s" % (c.name, c.topic)) + + +def command_today(msg_from: str, message: str): + today = str(datetime.now().day) + day_start = datetime.today().replace(hour=6, minute=0, second=0) + + today_list = [] + for member, member_data in cache.items(): + if not member.startswith("__") and 'days' in member_data and today in member_data['days']: + today_list.append(member) + + ircbot.privmsg( + IRC_CHANNEL, + "Todays leaderboard (last updated: %s):" % cache['__last_update__'] + ) + for i, member in enumerate(sorted(today_list, key=lambda x: cache[x]['days'][today]['score'], reverse=True)): + if i > 3: + sleep(1) # don't flood + + if "1" in cache[member]['days'][today]: + p1_time = "in " + human_readable_time_from_delta( + datetime.fromisoformat(cache[member]['days'][today]['1']) - day_start + ) + else: + p1_time = "not yet solved" + + if "2" in cache[member]['days'][today]: + p2_time = "in " + human_readable_time_from_delta( + datetime.fromisoformat(cache[member]['days'][today]['2']) - day_start + ) + else: + p2_time = "not yet solved" + + ircbot.privmsg( + IRC_CHANNEL, + "%d) %s (Scores: total: %d, today: %d) p1 %s, p2 %s" + % ( + i + 1, + cache[member]['name'], + cache[member]['score'], + cache[member]['days'][today]['score'], + p1_time, + p2_time + ) + ) + + +def command_quit(msg_from: str, message: str): + if msg_from.startswith("stha!") or msg_from.startswith("Pennywise!"): + ircbot.privmsg(msg_from, "Oh, ok ... bye :'(") + ircbot.quit() + sys.exit(0) + + +def on_raw(msg_from: str, msg_type: str, msg_to: str, message: str): + print("[%s] <%s> (%s) -> <%s>: %s" % (datetime.now().strftime("%H:%M:%S"), msg_from, msg_type, msg_to, message)) + + +def calc_scores(): + member_count = len([x for x in cache.keys() if not x.startswith("__")]) + for day in map(str, range(1, 26)): + p1_times = [] + p2_times = [] + for member, member_data in cache.items(): + if member.startswith("__") or day not in member_data['days']: + continue + + cache[member]['days'][day]['score'] = 0 + if '1' in member_data['days'][day]: + p1_times.append(member_data['days'][day]['1']) + if '2' in member_data['days'][day]: + p2_times.append(member_data['days'][day]['2']) + + for member, member_data in cache.items(): + if member.startswith("__") or day not in member_data['days']: + continue + + if '1' in member_data['days'][day] and member_data['days'][day]['1'] in p1_times: + score = member_count - sorted(p1_times).index(member_data['days'][day]['1']) + cache[member]['days'][day]['score'] += score + + if '2' in member_data['days'][day] and member_data['days'][day]['2'] in p2_times: + score = member_count - sorted(p2_times).index(member_data['days'][day]['2']) + cache[member]['days'][day]['score'] += score + + +def update_leaderboard(): + try: + new_leaderboard = fetch_leaderboard() + except: + return # didn't work this time? Well, we'll just try again in 15min ... + + now = datetime.now() + + new_stars = {} + for member, member_data in new_leaderboard['members'].items(): + if member not in cache: + cache[member] = { + 'name': member_data['name'], + 'days': {}, + } + + cache[member]['global_score'] = int(member_data['global_score']) + cache[member]['score'] = int(member_data['local_score']) + cache[member]['stars'] = int(member_data['stars']) + for day in member_data['completion_day_level']: + day_start = datetime(now.year, 12, int(day), 6, 0, 0) + if day not in cache[member]['days']: + cache[member]['days'][day] = {} + + for part in member_data['completion_day_level'][day]: + if part not in cache[member]['days'][day]: + completion_time = datetime.fromtimestamp( + member_data['completion_day_level'][day][part]['get_star_ts'] + ) + if member_data['name'] not in new_stars: + new_stars[member_data['name']] = {} + + finishing_time = human_readable_time_from_delta(completion_time - day_start) + new_stars[member_data['name']]['d' + day + 'p' + part] = finishing_time + cache[member]['days'][day][part] = completion_time.isoformat() + + if len(new_stars) > 0: + ircbot.privmsg(IRC_CHANNEL, "New Stars found:") + for member, parts in new_stars.items(): + line = member + ": " + line += ", ".join( + "%s (%s)" + % (part, new_stars[member][part]) for part in sorted(new_stars[member].keys()) + ) + + ircbot.privmsg(IRC_CHANNEL, line) + + cache['__last_update__'] = datetime.now().isoformat() + cache.save() + calc_scores() + + +cache = JSONFile("aocrr_bot.cache", create=True) +session_id = open(".session", "r").readlines()[0].strip() + +ircbot = IrcBot(IRC_SERVER, IRC_PORT, IRC_NICK, IRC_USER, IRC_REALNAME) +ircbot.join(IRC_CHANNEL) +ircbot.schedule('update_leaderboard', timedelta(minutes=15), update_leaderboard) +ircbot.on(ServerMessage.RAW, on_raw) +ircbot.register_channel_command("!info", IRC_CHANNEL, command_info) +ircbot.register_channel_command("!today", IRC_CHANNEL, command_today) +ircbot.register_privmsg_command("info", command_info) +ircbot.register_privmsg_command("quit", command_quit) +ircbot.run() diff --git a/leaderboard.py b/leaderboard.py new file mode 100644 index 0000000..8fe4761 --- /dev/null +++ b/leaderboard.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +import datetime +import json +import requests + +year = 2022 + + +def lpad(string: str, length: int) -> str: + return " " * (length - len(string)) + string + + +session_id = open(".session", "r").readlines()[0].strip() +leaderboard = json.loads( + requests.get( + "https://adventofcode.com/%d/leaderboard/private/view/711147.json" % year, + cookies={'session': session_id} + ).content +) + +member_score = {} +max_day = 1 +now = datetime.datetime.now() +for member_id in leaderboard['members']: + if leaderboard['members'][member_id]['local_score'] == 0: + continue + + sort_score = leaderboard['members'][member_id]['local_score'] * 100 + leaderboard['members'][member_id]['stars'] + for day in leaderboard['members'][member_id]['completion_day_level']: + if int(day) > max_day: + max_day = int(day) + while sort_score in member_score: + sort_score += 1 + member_score[sort_score] = member_id + +member_order = [member_score[x] for x in sorted(member_score.keys(), reverse=True)] + +print(" Day", end="") +for member_id in member_order: + print( + " |", + leaderboard['members'][member_id]['name'] + " " * (8 - len(leaderboard['members'][member_id]['name'])), + end="" + ) +print() +print(" ", end="") +for member_id in member_order: + print( + " |", + lpad( + "%2d / %3d" + % ( + leaderboard['members'][member_id]['stars'], + leaderboard['members'][member_id]['local_score'], + ), + max(8, len(leaderboard['members'][member_id]['name'])) + ), + end="" + ) +print() +print("-----", end="") +for member_id in member_order: + print("+-" + "-" * max(8, len(leaderboard['members'][member_id]['name'])) + "-", end="") +print() + + +def time_from_delta(delta: datetime.timedelta) -> str: + if delta.days > 0: + return "%d days" % delta.days + + time_str = "" + if delta.seconds > 3600: + time_str += "%02d:" % (delta.seconds // 3600) + else: + time_str += "00:" + + if delta.seconds % 3600 > 60: + time_str += "%02d:" % (delta.seconds % 3600 // 60) + else: + time_str += "00:" + + return time_str + "%02d" % (delta.seconds % 60) + + +for day in range(max_day, 0, -1): + print(" %3d" % day, end="") + day_start = datetime.datetime(year, 12, day, 6, 0, 0) + for member_id in member_order: + member_name = leaderboard['members'][member_id]['name'] + try: + star_1_time = datetime.datetime.fromtimestamp( + leaderboard['members'][member_id]['completion_day_level'][str(day)]["1"]['get_star_ts'] + ) + except KeyError: + star_1_time = None + + print( + " |", + lpad(time_from_delta(star_1_time - day_start), len(member_name)) + if star_1_time is not None + else lpad("not yet", max(8, len(member_name))), + end="" + ) + + print() + print(" ", end="") + for member_id in member_order: + member_name = leaderboard['members'][member_id]['name'] + try: + star_2_time = datetime.datetime.fromtimestamp( + leaderboard['members'][member_id]['completion_day_level'][str(day)]["2"]['get_star_ts'] + ) + except KeyError: + star_2_time = None + + print( + " |", + lpad(time_from_delta(star_2_time - day_start), len(member_name)) + if star_2_time is not None + else lpad("not yet", max(8, len(member_name))), + end="" + ) + + print() diff --git a/main.py b/main.py new file mode 100644 index 0000000..c833c0e --- /dev/null +++ b/main.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 + +import tools.aoc +import argparse +import importlib +import os + +YEAR = 2022 +TIMEIT_NUMBER = 50 + +argument_parser = argparse.ArgumentParser() +argument_parser.add_argument("-d", "--day", help="specify day to process; leave empty for ALL days", type=int) +argument_parser.add_argument("-p", "--part", help="run only part x", choices=[1, 2], type=int) +argument_parser.add_argument("--timeit", help="measure execution time", action="store_true", default=False) +argument_parser.add_argument( + "--timeit-number", + help="build average time over this many executions", + type=int, + default=TIMEIT_NUMBER +) +argument_parser.add_argument("-v", "--verbose", help="show test case outputs", action="store_true", default=False) +flags = argument_parser.parse_args() + +import_day = "" +if flags.day: + import_day = "%02d" % flags.day + +imported = [] +for _, _, files in os.walk(tools.aoc.BASE_PATH): + for f in files: + if f.startswith('day' + import_day) and f.endswith('.py'): + lib_name = f[:-3] + globals()[lib_name] = importlib.import_module(lib_name) + imported.append(lib_name) + + break + +for lib in sorted(imported): + day = int(lib[-2:]) + day_class = getattr(globals()[lib], "Day")(YEAR, day) + day_class.run(flags.part if flags.part else 3, flags.verbose, flags.timeit, flags.timeit_number) diff --git a/skel_day.py b/skel_day.py new file mode 100644 index 0000000..dcebd15 --- /dev/null +++ b/skel_day.py @@ -0,0 +1,24 @@ +from tools.aoc import AOCDay +from typing import Any + + +class Day(AOCDay): + inputs = [ + [ + (None, "input%DAY%") + ], + [ + (None, "input%DAY%") + ] + ] + + def part1(self) -> Any: + return "" + + def part2(self) -> Any: + return "" + + +if __name__ == '__main__': + day = Day(%YEAR%, %DAY%) + day.run(verbose=True) diff --git a/start_day.py b/start_day.py new file mode 100644 index 0000000..a73ec03 --- /dev/null +++ b/start_day.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +from argparse import ArgumentParser +from datetime import datetime +from os.path import exists +from platform import system +from subprocess import call +from time import sleep +import webbrowser + + +YEAR = 2022 +CHARMS = { + 'Linux': '/usr/local/bin/charm', + 'Windows': r'C:\Program Files\JetBrains\PyCharm 2020.2.4\bin\pycharm64.exe' +} + +arg_parser = ArgumentParser() +arg_parser.add_argument("-d", "--day", help="start a specific day (default: today)", type=int) +args = arg_parser.parse_args() + +DAY = args.day or datetime.now().day + +if YEAR < 2015 or not 1 <= DAY <= 25: + print("Invalid year or day for year: %d, day: %d" % (YEAR, DAY)) + exit() + +day_file = "day%02d.py" % DAY +if exists(day_file): + print(day_file, "already exists. Use that one!") + exit() + +with open("skel_day.py", "r") as IN: + with open(day_file, "w") as OUT: + while in_line := IN.readline(): + OUT.write(in_line.replace("%YEAR%", str(YEAR)).replace("%DAY%", str(DAY))) + +start = datetime(YEAR, 12, DAY, 6, 0, 0) +now = datetime.now() +if start > now: + time_wait = start - now + if time_wait.days > 0: + print("Do you really want to wait %d days?" % time_wait.days) + exit() + + for x in range(time_wait.seconds, -1, -1): + print("Day starts in %02ds.\r") + sleep(1) + +call([CHARMS[system()], day_file]) +webbrowser.open("https://adventofcode.com/%d/day/%d" % (YEAR, DAY)) +call(["git", "add", day_file])