From 13fc8f280095082df2137c3c902ecc76e55775c3 Mon Sep 17 00:00:00 2001 From: Jacob Liu Date: Mon, 8 Apr 2024 00:20:00 +0800 Subject: [PATCH 01/20] Protect main branch in pre-commit. Also added comments to pre-commit config --- .pre-commit-config.yaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ed58941..f5ff8ae 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,11 +3,13 @@ repos: rev: v4.5.0 hooks: - id: check-yaml - - id: end-of-file-fixer - - id: trailing-whitespace - - id: fix-byte-order-marker - - id: mixed-line-ending + - id: end-of-file-fixer # always ensure a single newline at the end of a file + - id: trailing-whitespace # always remove trailing whitespace + - id: fix-byte-order-marker # always remove UTF-8 BOM + - id: mixed-line-ending # always convert to LF args: ["--fix=lf"] + - id: no-commit-to-branch # prevent commits to main branch + args: ["--branch", "main"] - repo: https://github.com/psf/black rev: 24.3.0 hooks: From a717a34da34c536c733f4bae586e62b3444d0ce9 Mon Sep 17 00:00:00 2001 From: Jacob Liu Date: Tue, 9 Apr 2024 13:55:23 +0800 Subject: [PATCH 02/20] generate_excel code cleanup --- generate_excel.py | 94 ++++++++++++++++++++++------------------------- 1 file changed, 44 insertions(+), 50 deletions(-) diff --git a/generate_excel.py b/generate_excel.py index 2e6a7d8..b986677 100644 --- a/generate_excel.py +++ b/generate_excel.py @@ -28,57 +28,45 @@ def preprocess_songlist(raw): transition = requests.get(URLTEMPLATE.format("Template:Transition.json")).json() -def disambiguateName(name, songid): +def disambiguate_name(name: str, songid: str) -> str: if name in transition["sameName"]: name = transition["sameName"][name][songid] return name -def getLinkName(songid): +def get_link_name(songid: str) -> str: name = songlist[songid]["title_localized"]["en"] # convert to display name if possible name = transition["songNameToDisplayName"].get(name, name) - return disambiguateName(name, songid) + return disambiguate_name(name, songid) -def getConstants(songid): - raw = chartconstant[songid] - constants = [entry.get("constant", pd.NA) if entry else pd.NA for entry in raw] - for _ in range(5 - len(constants)): - constants.append(pd.NA) - return constants - - -def getAllFlat(): - songids = list(chartconstant.keys()) - - matrix = [] - for songid in songids: - constants = getConstants(songid) - song = { - "songid": songid, - "linkName": getLinkName(songid), - "title": songlist[songid]["title_localized"][ - "en" - ], # will get disambiguated later - } - for i, diff in enumerate(DIFFICULTIES): - song_diff = song.copy() - # I hate Lowiro for this stupid json structure - # try to see if there's a difficulty-specific title - diffs = songlist[songid]["difficulties"] - diffclasses = [dff["ratingClass"] for dff in diffs] - if i not in diffclasses: +def get_all_entries() -> pd.DataFrame: + rows = [] + for songid, song_info in songlist.items(): + for difficulty_record in song_info["difficulties"]: + if difficulty_record["rating"] == 0: + # Last | Eternity has three 0-rated non-existent charts continue - diffsInd = diffclasses.index(i) - if "title_localized" in diffs[diffsInd]: - song_diff["title"] = diffs[diffsInd]["title_localized"]["en"] - song_diff["title"] = disambiguateName(song_diff["title"], songid) - song_diff["label"] = diff.upper() - song_diff["detail"] = constants[i] - matrix.append(song_diff) - - return pd.DataFrame.from_records(matrix, index=["songid", "label"]).dropna() + if difficulty_record.get("hidden_until", "never") == "always": + # Skip other hidden charts for future-proofing + continue + ratingClass: int = difficulty_record["ratingClass"] + title: str = song_info["title_localized"]["en"] + if "title_localized" in difficulty_record: + # If the difficulty has a different name, use that + title = difficulty_record["title_localized"]["en"] + rows.append( + { + "songid": songid, + "label": DIFFICULTIES[ratingClass], + "title": disambiguate_name(title, songid), + "detail": chartconstant[songid][ratingClass]["constant"], + "linkName": get_link_name(songid), + } + ) + + return pd.DataFrame.from_records(rows, index=["songid", "label"]).dropna() def make_backup(filename): @@ -98,11 +86,12 @@ def make_backup(filename): def main(): FILE_NAME = "put_your_score_in_here.xlsx" SHEET_NAME = "Sheet1" - df = getAllFlat() + df = get_all_entries() df = df[df["detail"] >= 8] make_backup(FILE_NAME) + # Read the old scores and merge them try: df_in = pd.read_excel(FILE_NAME).set_index(["songid", "label"]) df_in = df_in[["score"]] @@ -115,11 +104,12 @@ def main(): ) print(e) - print(df_output) df_output.reset_index(inplace=True) df_output["name"] = df_output["title"] df_output["name_for_sorting"] = df_output["name"].str.upper() + # Xlsxwriter hackery: write links first, then write the text + # And it will still point to the correct link df_output["title"] = "https://arcwiki.mcd.blue/" + df_output["linkName"] df_output.sort_values( @@ -147,24 +137,28 @@ def main(): base_format = workbook.add_format( {"font_name": "calibri", "valign": "top", "align": "center"} ) + # Write the names over the links; this preserves the link worksheet.write_column(1, 0, df_output["name"], workbook.get_default_url_format()) - worksheet.set_column(0, 0, 50) - worksheet.set_column(1, 2, 8, base_format) - worksheet.set_column(3, 3, 15, base_format) + worksheet.set_column(0, 0, 50) # Names (links) + worksheet.set_column(1, 2, 8, base_format) # Difficulty label and detail constant + worksheet.set_column(3, 3, 15, base_format) # Score, wide since max ~1e7 worksheet.set_column(4, 4, None, None, {"hidden": True}) # hide songid column + + # Format the header row for col_num, value in enumerate(OUTPUT_COLUMNS): worksheet.write(0, col_num, value, header_format) + # Conditional coloring for difficulty for difficulty, color in DIFFICULTY_COLORS.items(): worksheet.conditional_format( - 1, - 1, - len(df), - 1, + 1, # Row 2 + 1, # Column B + len(df), # Last row + 1, # Column B { "type": "cell", "criteria": "equal to", - "value": f'"{difficulty.upper()}"', + "value": f'"{difficulty}"', "format": workbook.add_format({"bg_color": color}), }, ) From ffc7441e155413889de1d6740d85badac7eac37b Mon Sep 17 00:00:00 2001 From: Jacob Liu Date: Tue, 9 Apr 2024 13:55:27 +0800 Subject: [PATCH 03/20] gitignore put_your_score_in_here_backup.xlsx --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index de0d3bb..60b89a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .vscode/ put_your_score_in_here.xlsx +put_your_score_in_here_backup.xlsx # The rest is taken from https://github.com/github/gitignore/blob/main/Python.gitignore # Byte-compiled / optimized / DLL files From 62a22fef34ef6078a5866c15858f18264f96be97 Mon Sep 17 00:00:00 2001 From: FV1932 Date: Tue, 9 Apr 2024 14:53:26 +0800 Subject: [PATCH 04/20] user-ptt-input add user input ptt and split draw his function --- .gitignore | 1 + README.md | 6 +- b616.py | 94 +++++++++++++++++------------- history_b30.csv => ptt_history.csv | 0 4 files changed, 58 insertions(+), 43 deletions(-) rename history_b30.csv => ptt_history.csv (100%) diff --git a/.gitignore b/.gitignore index de0d3bb..01b6db4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .vscode/ put_your_score_in_here.xlsx +.b616venv # The rest is taken from https://github.com/github/gitignore/blob/main/Python.gitignore # Byte-compiled / optimized / DLL files diff --git a/README.md b/README.md index b8ac8c3..36b1fc7 100644 --- a/README.md +++ b/README.md @@ -14,14 +14,14 @@ ## Usage Instructions 首次使用: -1. 右键 `history_b30.csv`, 属性, 打开方式改为'记事本' (或其他plain text editor), +1. 右键 `ptt_history.csv`, 属性, 打开方式改为'记事本' (或其他plain text editor), 2. 运行 `generate_excel.py`. \*需要网络链接, 请耐心等待. 该程序会在当前目录下创建一个叫做 `put_your_score_in_here.xlsx` 的文件, 此 `xlsx` 请用 `MS Excel` 打开, 或者其他表格编辑器如 `numbers` 或者 `wps`, 拓展名请固定为 `.xlsx`. 3. 编辑 `put_your_score_in_here.xlsx`, 在score列输入分数, 无需全都填写, 没打的/不需要参与计算的留空不填就成. 4. 运行 `b616.py`, 第一行输入的数字为参与计算的数据量. -该输入会调整均值计算和图表生成的范围, 不会影响历史b30输入和推分推荐, 如果小于30则不会计算b30. +该输入会调整均值计算和图表生成的范围, 不会影响历史ptt输入和推分推荐, 如果小于30则不会计算b30. 其他: @@ -29,4 +29,4 @@ 运行后请检查 `put_your_score_in_here.xlsx` , 如果出现数据丢失, 结构错误等问题, 请 *不要* 重复运行程序. 更新前的数据放在同目录下的 `scores_here_backup.xlsx` , 请单独存好并在群里@GK说明 `generate_excel` 错误, 或者提issue. -2. 如果不小心记录了错误/不需要的历史b30数据, 可以直接(用记事本)打开 `history_b30.csv` 修改数据. +2. 如果不小心记录了错误/不需要的历史ptt数据, 可以直接(用记事本)打开 `ptt_history.csv` 修改数据. \ No newline at end of file diff --git a/b616.py b/b616.py index df557d9..e74573e 100644 --- a/b616.py +++ b/b616.py @@ -12,7 +12,7 @@ ) # 设定制表使用的字库 -# 以下为读取本地数据和用户输入模块 +# 以下为读取本地数据和用户自定义模块 def xlsx_tolist(): xlsx = pd.read_excel(r"put_your_score_in_here.xlsx") xlsx = xlsx[["title", "label", "detail", "score"]] @@ -21,9 +21,7 @@ def xlsx_tolist(): def cust_input(): - custom_num = int( - input("Hi, B616 here, 请输入显示/计算的成绩行数 e.g.30: ") - ) # 让用户决定需要查看的行数 + custom_num = int(input("Hi, B616 here, 请输入显示/计算的成绩行数 e.g.30: ")) if custom_num <= 0 or custom_num > len(in_list): print("输入量不符合数据量, 重来") time.sleep(0.5) @@ -31,6 +29,21 @@ def cust_input(): return custom_num +def read_ptt_history_csv(): + with open("ptt_history.csv", mode="r", newline="") as f: + reader = csv.reader(f) + + x_time = [] # x-axis 年月日时间 + y_realptt = [] # y-axis 用户输入的实际ptt + y_maxiptt = [] # y-axis r10=b10时的理论最高ptt + for row in reader: + x_time.append(row[0]) + y_realptt.append(float(row[1])) + y_maxiptt.append(float(row[2])) + + return x_time, y_realptt, y_maxiptt + + # 以下为排序和计算模块 def get_desc_list(): invalid_score = [] @@ -63,10 +76,10 @@ def get_desc_list(): time.sleep(0.5) input("Press enter to continue") # 最后根据rating和detail(定数)分别进行逆向排序并返回 - return [ + return ( sorted(in_list, key=lambda s: s[4], reverse=True), # rating sorted(in_list, key=lambda s: s[2], reverse=True), # detail - ] + ) def get_cust_avg(): @@ -83,15 +96,19 @@ def get_b30_avg(): restb30_sum = 0 for row in desc_ra_list[10:30]: restb30_sum += row[4] - return [ + return ( round((b10_sum + restb30_sum) / 30, 4), # 纯b30底分 round((b10_sum * 2 + restb30_sum) / 40, 4), # r10=b10时的最高ptt计算公式 - ] + ) -def write_history_b30_csv(): - with open("history_b30.csv", mode="a", newline="") as f: - line = [str(b30_withr10), time.strftime("%Y/%m/%d", time.localtime())] +def write_ptt_history_csv(): + with open("ptt_history.csv", mode="a", newline="") as f: + line = [ + time.strftime("%Y/%m/%d", time.localtime()), + str(real_ptt_input), + str(b30_withr10), + ] writer = csv.writer(f) writer.writerow(line) @@ -313,50 +330,47 @@ def score2detail_chart(): def draw_history_b30_chart(): - with open("history_b30.csv", mode="r", newline="") as f: - reader = csv.reader(f) - - x_time = [] # x-axis 年月日时间 - y_b30r = [] # y-axis 带有理论最高r10的b30 - for row in reader: - x_time.append(row[1]) - y_b30r.append(float(row[0])) - - if len(x_time) == 0: - print("历史b30数据为空, 跳过生成b30折线图") - return - - x_time = [datetime.strptime(d, "%Y/%m/%d").strftime("%Y/%m/%d") for d in x_time] + line = read_ptt_history_csv() # 打开csv并读取用户过去填入的数据 + x_time = line[0] + if len(x_time) == 0: + print("ptt_history数据为空, 跳过生成b30折线图") + time.sleep(0.5) + return + x_time = [datetime.strptime(d, "%Y/%m/%d").strftime("%Y/%m/%d") for d in x_time] + y_realptt = line[1] + y_maxiptt = line[2] - dot_size = max((50 - pow(len(y_b30r), 0.5)), 10) - plt.scatter(x_time, y_b30r, s=dot_size) - plt.tick_params(axis="x", labelrotation=61.6) - plt.xlabel("年/月/日", fontsize=12) - plt.ylabel("不推b30, 即r10=b10时的PTT", fontsize=11) - plt.show() + dot_size = max((50 - pow(len(y_maxiptt), 0.5)), 10) + plt.scatter(x_time, y_realptt, s=dot_size) + plt.scatter(x_time, y_maxiptt, s=dot_size) + plt.tick_params(axis="x", labelrotation=61.6) + plt.xlabel("年/月/日", fontsize=12) + plt.ylabel("ptt", fontsize=12) + plt.legend(["实际输入ptt", "理论最高ptt"], loc="best") + plt.show() if __name__ == "__main__": + logging.getLogger("matplotlib.font_manager").setLevel( logging.ERROR ) # 忽略字体Error级以下的报错 + in_list = xlsx_tolist() # 读入xlsx文件转换成标准list custom_num = cust_input() # 让用户输入想要查看的成绩数量 desc_list = get_desc_list() # 数据有效性检查, 计算各曲rating - desc_ra_list = desc_list[0] # 根据rating排序 (单曲ptt) - desc_dt_list = desc_list[1] # 根据detail排序 (谱面定数) + desc_ra_list = desc_list[0] # rating倒序list (单曲ptt) + desc_dt_list = desc_list[1] # detail倒序list (谱面定数) cust_average = get_cust_avg() # 根据用户输入的成绩数量计算rating均值 if custom_num >= 30: # 如果用户输入数量至少为30则: - b30 = get_b30_avg() # 计算b30 - b30_only = b30[0] # 仅b30的平均底分 + real_ptt_input = float(input("请输入当前您的实际ptt(例 12.47): ")) + b30 = get_b30_avg() # 计算b30并return以下两个数据: + b30_only = b30[0] # 仅考虑b30底分的ptt b30_withr10 = b30[1] # r10=b10时的理论最高ptt - if ( - input("是否要用本次的数据更新历史b30数据(Y/N): ").upper() - == "Y" # y和Y都会确认 - ): # 如果用户确认: - write_history_b30_csv() # 则把本次的 b30_withr10 加入历史记录, 用来生成变化图像 + if input("是否要更新历史ptt数据(Y/N): ").upper() == "Y": # by和Y都会确认 + write_ptt_history_csv() # 把 real_ptt_input和b30_withr10 存档, 用来生成变化图像 print() show_desc_ra_list() # 展示根据rating排序的分数列表 diff --git a/history_b30.csv b/ptt_history.csv similarity index 100% rename from history_b30.csv rename to ptt_history.csv From b0f660894a0ab4f2dd8fc0facfe98798a732296f Mon Sep 17 00:00:00 2001 From: Jacob Liu Date: Tue, 9 Apr 2024 20:38:31 +0800 Subject: [PATCH 05/20] Better default sorting: label, then rating class, then name --- generate_excel.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/generate_excel.py b/generate_excel.py index b986677..4fcbb58 100644 --- a/generate_excel.py +++ b/generate_excel.py @@ -41,6 +41,15 @@ def get_link_name(songid: str) -> str: return disambiguate_name(name, songid) +def get_detail_for_sorting(difficulty_record) -> float: + base_difficulty = float(difficulty_record["rating"]) + rating_plus = difficulty_record.get("ratingPlus", False) + if rating_plus: + # just for sorting purposes, such that 9 < 9+ < 10 + base_difficulty += 0.5 + return base_difficulty + + def get_all_entries() -> pd.DataFrame: rows = [] for songid, song_info in songlist.items(): @@ -63,6 +72,7 @@ def get_all_entries() -> pd.DataFrame: "title": disambiguate_name(title, songid), "detail": chartconstant[songid][ratingClass]["constant"], "linkName": get_link_name(songid), + "detail_for_sorting": get_detail_for_sorting(difficulty_record), } ) @@ -113,7 +123,9 @@ def main(): df_output["title"] = "https://arcwiki.mcd.blue/" + df_output["linkName"] df_output.sort_values( - ["label", "name_for_sorting"], inplace=True, ascending=[True, True] + ["label", "detail_for_sorting", "name_for_sorting"], + inplace=True, + ascending=[True, False, True], ) OUTPUT_COLUMNS = ["title", "label", "detail", "score", "songid"] From 557f1e6e16bd4c47ee1648c5d91a9eddb040c408 Mon Sep 17 00:00:00 2001 From: FV1932 Date: Wed, 10 Apr 2024 11:24:58 +0800 Subject: [PATCH 06/20] change xlsx file name change sleep time to 1sec --- README.md | 6 +++--- b616.py | 18 +++++++++--------- generate_excel.py | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 36b1fc7..e86fbff 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@ 1. 右键 `ptt_history.csv`, 属性, 打开方式改为'记事本' (或其他plain text editor), 2. 运行 `generate_excel.py`. \*需要网络链接, 请耐心等待. -该程序会在当前目录下创建一个叫做 `put_your_score_in_here.xlsx` 的文件, +该程序会在当前目录下创建一个叫做 `put_your_score_here.xlsx` 的文件, 此 `xlsx` 请用 `MS Excel` 打开, 或者其他表格编辑器如 `numbers` 或者 `wps`, 拓展名请固定为 `.xlsx`. -3. 编辑 `put_your_score_in_here.xlsx`, +3. 编辑 `put_your_score_here.xlsx`, 在score列输入分数, 无需全都填写, 没打的/不需要参与计算的留空不填就成. 4. 运行 `b616.py`, 第一行输入的数字为参与计算的数据量. 该输入会调整均值计算和图表生成的范围, 不会影响历史ptt输入和推分推荐, 如果小于30则不会计算b30. @@ -26,7 +26,7 @@ 其他: 1. 如果Arcaea(wiki)有更新, 运行 `generate_excel.py`, 通常能一步到位更新. -运行后请检查 `put_your_score_in_here.xlsx` , 如果出现数据丢失, 结构错误等问题, 请 *不要* 重复运行程序. +运行后请检查 `put_your_score_here.xlsx` , 如果出现数据丢失, 结构错误等问题, 请 *不要* 重复运行程序. 更新前的数据放在同目录下的 `scores_here_backup.xlsx` , 请单独存好并在群里@GK说明 `generate_excel` 错误, 或者提issue. 2. 如果不小心记录了错误/不需要的历史ptt数据, 可以直接(用记事本)打开 `ptt_history.csv` 修改数据. \ No newline at end of file diff --git a/b616.py b/b616.py index e74573e..ce62607 100644 --- a/b616.py +++ b/b616.py @@ -14,7 +14,7 @@ # 以下为读取本地数据和用户自定义模块 def xlsx_tolist(): - xlsx = pd.read_excel(r"put_your_score_in_here.xlsx") + xlsx = pd.read_excel(r"put_your_score_here.xlsx") xlsx = xlsx[["title", "label", "detail", "score"]] xlsx.dropna(inplace=True) # 删除未填入分数的行 credits to GK return xlsx.values.tolist() @@ -61,19 +61,19 @@ def get_desc_list(): continue if score >= 9800000: # 认认真真的推分哥, salute! 请保持下去, 飞升之路就在脚下 - rating = detail + 1 + (score - 9800000) / 200000 - row.append(round(rating, 4)) + rating = detail + (score-9800000)/200000 + 1 + row.append(rating) continue if score >= 1002237: # 没有ex还想吃分? ex以下单曲rating低得可怜, 能不能推推 - rating = detail + (score - 9500000) / 300000 - rating = max(rating, 0) # 理论上是真的有可能出现的, 我猜应该是你家猫打的 ( - row.append(round(rating, 4)) + rating = detail + (score-9500000)/300000 + rating = max(rating, 0) # 0单曲ra真的有可能出现的, 我猜是你家猫打的( + row.append(rating) continue if len(invalid_score) != 0: # 如果有出现不合法的score输入: for error_row in invalid_score: - print("Your score of ", error_row, " is probably invaild, please check") - time.sleep(0.5) + print("Your score of ", error_row[0], " maybe invaild, please check in xlsx.") + time.sleep(1) input("Press enter to continue") # 最后根据rating和detail(定数)分别进行逆向排序并返回 return ( @@ -334,7 +334,7 @@ def draw_history_b30_chart(): x_time = line[0] if len(x_time) == 0: print("ptt_history数据为空, 跳过生成b30折线图") - time.sleep(0.5) + time.sleep(1) return x_time = [datetime.strptime(d, "%Y/%m/%d").strftime("%Y/%m/%d") for d in x_time] y_realptt = line[1] diff --git a/generate_excel.py b/generate_excel.py index 4fcbb58..a2cb038 100644 --- a/generate_excel.py +++ b/generate_excel.py @@ -94,7 +94,7 @@ def make_backup(filename): def main(): - FILE_NAME = "put_your_score_in_here.xlsx" + FILE_NAME = "put_your_score_here.xlsx" SHEET_NAME = "Sheet1" df = get_all_entries() df = df[df["detail"] >= 8] From 09cfa408f0b63545a55db379d292f3c5f99ab617 Mon Sep 17 00:00:00 2001 From: FV1932 Date: Thu, 11 Apr 2024 10:56:35 +0800 Subject: [PATCH 07/20] filename_listorder_round change sleep time to 1sec change local xlsx file name change round-while-store to format-when-print --- .gitignore | 6 ++--- b616.py | 79 +++++++++++++++++++++++++++++++++--------------------- 2 files changed, 52 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index 78d93ad..795e432 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ .vscode/ -put_your_score_in_here.xlsx -put_your_score_in_here_backup.xlsx -.b616venv +ptt_history.csv +put_your_score_here.xlsx +put_your_score_here_backup.xlsx # The rest is taken from https://github.com/github/gitignore/blob/main/Python.gitignore diff --git a/b616.py b/b616.py index ce62607..f8b1c90 100644 --- a/b616.py +++ b/b616.py @@ -35,13 +35,15 @@ def read_ptt_history_csv(): x_time = [] # x-axis 年月日时间 y_realptt = [] # y-axis 用户输入的实际ptt + y_baseptt = [] # y-axis 仅b30底分的ptt y_maxiptt = [] # y-axis r10=b10时的理论最高ptt for row in reader: x_time.append(row[0]) y_realptt.append(float(row[1])) - y_maxiptt.append(float(row[2])) + y_baseptt.append(float(row[2])) + y_maxiptt.append(float(row[3])) - return x_time, y_realptt, y_maxiptt + return x_time, y_realptt, y_baseptt, y_maxiptt # 以下为排序和计算模块 @@ -61,18 +63,18 @@ def get_desc_list(): continue if score >= 9800000: # 认认真真的推分哥, salute! 请保持下去, 飞升之路就在脚下 - rating = detail + (score-9800000)/200000 + 1 + rating = detail + (score - 9800000) / 200000 + 1 row.append(rating) continue if score >= 1002237: # 没有ex还想吃分? ex以下单曲rating低得可怜, 能不能推推 - rating = detail + (score-9500000)/300000 + rating = detail + (score - 9500000) / 300000 rating = max(rating, 0) # 0单曲ra真的有可能出现的, 我猜是你家猫打的( row.append(rating) continue if len(invalid_score) != 0: # 如果有出现不合法的score输入: for error_row in invalid_score: - print("Your score of ", error_row[0], " maybe invaild, please check in xlsx.") + print("Your score of ", error_row[0], " maybe invaild, please check xlsx.") time.sleep(1) input("Press enter to continue") # 最后根据rating和detail(定数)分别进行逆向排序并返回 @@ -86,19 +88,19 @@ def get_cust_avg(): ra_sum = 0 for row in desc_ra_list[0:custom_num]: ra_sum += row[4] - return round((ra_sum / custom_num), 4) + return ra_sum / custom_num def get_b30_avg(): b10_sum = 0 for row in desc_ra_list[0:10]: b10_sum += row[4] # row[4]为上个方法append的单曲rating值 - restb30_sum = 0 + restb20_sum = 0 for row in desc_ra_list[10:30]: - restb30_sum += row[4] + restb20_sum += row[4] return ( - round((b10_sum + restb30_sum) / 30, 4), # 纯b30底分 - round((b10_sum * 2 + restb30_sum) / 40, 4), # r10=b10时的最高ptt计算公式 + (restb20_sum + b10_sum) / 30, # 纯b30底分 + (restb20_sum + b10_sum * 2) / 40, # r10=b10时的最高ptt计算公式 ) @@ -106,7 +108,8 @@ def write_ptt_history_csv(): with open("ptt_history.csv", mode="a", newline="") as f: line = [ time.strftime("%Y/%m/%d", time.localtime()), - str(real_ptt_input), + real_ptt_input, + str(b30_only), str(b30_withr10), ] writer = csv.writer(f) @@ -114,40 +117,53 @@ def write_ptt_history_csv(): # 以下为数据分析呈现模块 + + def show_desc_ra_list(): print() + + def print_rows(desc_ra_list): + print( + desc_ra_list[row_num][0], + desc_ra_list[row_num][1], + desc_ra_list[row_num][2], + " score:", + int(desc_ra_list[row_num][3]), + " rating:", + f"{desc_ra_list[row_num][4]:.4f}", + ) + row_num = 0 while ( row_num < 30 and row_num < custom_num ): # 没到B30th也没到设定的custom_num上限前 - print(desc_ra_list[row_num]) + print_rows(desc_ra_list) row_num += 1 print() if row_num == 30: - print("b30底分:", b30_only, " (忽略r10)") - print("不推b30, 也就是r10=b10时的理论最高ptt: ", b30_withr10) + print("b30底分:", f"{b30_only:.4f}", " (忽略r10)") + print("不推b30, 也就是r10=b10时的理论最高ptt: ", f"{b30_withr10:.4f}") print("---------b30 finished---------") else: # 指定数据量小于30的情况 - print(f"b{custom_num}底分:", cust_average, " (忽略r10)") + print(f"b{custom_num}底分:", f"{cust_average:.4f}", " (忽略r10)") print(f"---------b{custom_num} finished---------") if custom_num > 30: print() for row_num in range(30, custom_num): - print(desc_ra_list[row_num]) + print_rows(desc_ra_list) print() - print(f"b{custom_num}底分:", cust_average, " (忽略r10)") + print(f"b{custom_num}底分:", f"{cust_average:.4f}", " (忽略r10)") print(f"---------b{custom_num} finished---------") print() def suggest_song(): target_rating = desc_ra_list[30][4] # B30th的单曲rating, 超过这个就能推B30底分 - for i in range(min(len(desc_ra_list), 80), 30, -1): # 从B30th后的至多50行中随机挑选 - line = desc_ra_list[ - random.randint(30, i - 1) - ] # line=randint的范围会随着循环逐渐减少 - # 随机范围从30到len(desc_ra_list)或80, 每轮上限-1直到最后指定30, 所以接近B30th的分数有更高概率被选中 + for i in range(min(len(desc_ra_list), 80), 30, -1): + line = desc_ra_list[random.randint(30, i - 1)] + # line=randint的范围会随着循环逐渐从30到min(len(desc_ra_list), 80) + # 每轮上限-1直到最后指定30, 所以接近B30th的分数有更高概率被选中 detail = line[2] # 以下是一个比较取巧的判断方式, 通过目标rating和谱面定数之差确认能否推荐(能否满足if/elif) dt_ra_diff = target_rating - detail @@ -216,7 +232,7 @@ def rating2detail_chart(): y=b30_withr10, linewidth=1, # linewidth linestyle="-.", # linestyle - label=f"不推b30, r10=b10时的理论最高ptt:{b30_withr10}", + label=f"不推b30, r10=b10时的理论最高ptt:{b30_withr10:.4f}", ) # 图例 ax.legend(loc="best") # 自动调整图例到最佳位置 ################################################################ @@ -337,16 +353,18 @@ def draw_history_b30_chart(): time.sleep(1) return x_time = [datetime.strptime(d, "%Y/%m/%d").strftime("%Y/%m/%d") for d in x_time] + dot_size = max((50 - pow(len(x_time), 0.5)), 10) + y_realptt = line[1] y_maxiptt = line[2] - - dot_size = max((50 - pow(len(y_maxiptt), 0.5)), 10) + y_baseptt = line[3] plt.scatter(x_time, y_realptt, s=dot_size) + plt.scatter(x_time, y_baseptt, s=dot_size) plt.scatter(x_time, y_maxiptt, s=dot_size) plt.tick_params(axis="x", labelrotation=61.6) plt.xlabel("年/月/日", fontsize=12) plt.ylabel("ptt", fontsize=12) - plt.legend(["实际输入ptt", "理论最高ptt"], loc="best") + plt.legend(["真实ptt", "仅底分ptt", "理论最高ptt"], loc="best") plt.show() @@ -365,10 +383,11 @@ def draw_history_b30_chart(): cust_average = get_cust_avg() # 根据用户输入的成绩数量计算rating均值 if custom_num >= 30: # 如果用户输入数量至少为30则: - real_ptt_input = float(input("请输入当前您的实际ptt(例 12.47): ")) - b30 = get_b30_avg() # 计算b30并return以下两个数据: - b30_only = b30[0] # 仅考虑b30底分的ptt - b30_withr10 = b30[1] # r10=b10时的理论最高ptt + b30_pack = get_b30_avg() # 计算b30并return以下两个数据: + b30_only = b30_pack[0] # 仅考虑b30底分的ptt + b30_withr10 = b30_pack[1] # r10=b10时的理论最高ptt + + real_ptt_input = input("请输入当前您的实际ptt(例 12.47): ") if input("是否要更新历史ptt数据(Y/N): ").upper() == "Y": # by和Y都会确认 write_ptt_history_csv() # 把 real_ptt_input和b30_withr10 存档, 用来生成变化图像 print() From 3be602797e7db40e965c12b74321326a6fa66b5f Mon Sep 17 00:00:00 2001 From: Jacob Liu Date: Thu, 11 Apr 2024 14:24:32 +0800 Subject: [PATCH 08/20] Auto format using black on push and PR. --- .github/workflows/black.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .github/workflows/black.yml diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml new file mode 100644 index 0000000..3eb23a8 --- /dev/null +++ b/.github/workflows/black.yml @@ -0,0 +1,10 @@ +name: Format with Black + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: psf/black@stable From fc14302f3f63142530df22aee7785c4870379859 Mon Sep 17 00:00:00 2001 From: FV1932 Date: Thu, 11 Apr 2024 14:40:32 +0800 Subject: [PATCH 09/20] blank and invalid score print add score when print invalid input --- b616.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/b616.py b/b616.py index f8b1c90..a58e0fe 100644 --- a/b616.py +++ b/b616.py @@ -13,6 +13,8 @@ # 以下为读取本地数据和用户自定义模块 + + def xlsx_tolist(): xlsx = pd.read_excel(r"put_your_score_here.xlsx") xlsx = xlsx[["title", "label", "detail", "score"]] @@ -47,6 +49,8 @@ def read_ptt_history_csv(): # 以下为排序和计算模块 + + def get_desc_list(): invalid_score = [] for row in in_list: @@ -74,7 +78,13 @@ def get_desc_list(): continue if len(invalid_score) != 0: # 如果有出现不合法的score输入: for error_row in invalid_score: - print("Your score of ", error_row[0], " maybe invaild, please check xlsx.") + print( + "Your score of", + error_row[0], + "is", + int(error_row[3]), + ", which might be invaild, please check xlsx then run again", + ) time.sleep(1) input("Press enter to continue") # 最后根据rating和detail(定数)分别进行逆向排序并返回 From b071991feeaf3dfade973a601c08751d7dd0d3ed Mon Sep 17 00:00:00 2001 From: FV1932 Date: Thu, 11 Apr 2024 19:09:08 +0800 Subject: [PATCH 10/20] print rows credit to GK --- b616.py | 81 ++++++++++++++++++++++----------------------------------- 1 file changed, 31 insertions(+), 50 deletions(-) diff --git a/b616.py b/b616.py index a58e0fe..20055db 100644 --- a/b616.py +++ b/b616.py @@ -12,7 +12,7 @@ ) # 设定制表使用的字库 -# 以下为读取本地数据和用户自定义模块 +# 以下为读取本地数据和用户自定义函数 def xlsx_tolist(): @@ -48,7 +48,7 @@ def read_ptt_history_csv(): return x_time, y_realptt, y_baseptt, y_maxiptt -# 以下为排序和计算模块 +# 以下为排序和计算函数 def get_desc_list(): @@ -57,7 +57,7 @@ def get_desc_list(): score = row[3] # 玩家输入的得分(score) detail = row[2] # 谱面的细节定数(detail) if score >= 10002237 or score < 1002237: - # 不合法的score(2237为1far时score不超过10M的物量阈值) + # 不合法的score(2237为1far时score不超过10M的物量阈值) invalid_score.append(row) continue if score >= 10000000: @@ -82,16 +82,13 @@ def get_desc_list(): "Your score of", error_row[0], "is", - int(error_row[3]), + error_row[3], ", which might be invaild, please check xlsx then run again", ) time.sleep(1) input("Press enter to continue") - # 最后根据rating和detail(定数)分别进行逆向排序并返回 - return ( - sorted(in_list, key=lambda s: s[4], reverse=True), # rating - sorted(in_list, key=lambda s: s[2], reverse=True), # detail - ) + # 最后根据rating逆向排序并返回 + return sorted(in_list, key=lambda s: s[4], reverse=True) # rating def get_cust_avg(): @@ -126,46 +123,32 @@ def write_ptt_history_csv(): writer.writerow(line) -# 以下为数据分析呈现模块 +# 以下为数据分析呈现函数 def show_desc_ra_list(): print() - def print_rows(desc_ra_list): - print( - desc_ra_list[row_num][0], - desc_ra_list[row_num][1], - desc_ra_list[row_num][2], - " score:", - int(desc_ra_list[row_num][3]), - " rating:", - f"{desc_ra_list[row_num][4]:.4f}", - ) - - row_num = 0 - while ( - row_num < 30 and row_num < custom_num - ): # 没到B30th也没到设定的custom_num上限前 - print_rows(desc_ra_list) - row_num += 1 + def print_row(row): + print(f"{row[0]} {row[1]} {row[2]} score: {int(row[3])} rating: {row[4]:.4f}") + + rows = desc_ra_list[:custom_num] + for row in rows[:30]: + print_row(row) print() - if row_num == 30: + + if custom_num_over30: print("b30底分:", f"{b30_only:.4f}", " (忽略r10)") - print("不推b30, 也就是r10=b10时的理论最高ptt: ", f"{b30_withr10:.4f}") + print("r10=b10时的理论最高ptt: ", f"{b30_withr10:.4f}") print("---------b30 finished---------") - else: # 指定数据量小于30的情况 - print(f"b{custom_num}底分:", f"{cust_average:.4f}", " (忽略r10)") - print(f"---------b{custom_num} finished---------") - - if custom_num > 30: - print() - for row_num in range(30, custom_num): - print_rows(desc_ra_list) print() + + for row in rows[30:]: + print_row(row) + + if custom_num != 30: print(f"b{custom_num}底分:", f"{cust_average:.4f}", " (忽略r10)") print(f"---------b{custom_num} finished---------") - print() def suggest_song(): @@ -200,10 +183,10 @@ def suggest_song(): def draw_rt_sc_chart(): - sg_title = [] # song title(曲名) - x_detail = [] # x-axis detail(定数) - y_rating = [] # y-axis rating(单曲ptt) - y1_score = [] # y-axis score(单曲得分) + sg_title = [] # song title(曲名) + x_detail = [] # x-axis detail(定数) + y_rating = [] # y-axis rating(单曲ptt) + y1_score = [] # y-axis score(单曲得分) for row in desc_ra_list[0:custom_num]: sg_title.append(row[0]) x_detail.append(row[2]) @@ -233,16 +216,16 @@ def rating2detail_chart(): ) # 设置刻度标记的样式 ax.xaxis.set_major_locator( ticker.MultipleLocator(0.1) - ) # tick_spacing = 0.1: 横轴标注以0.1为单位步进 (即arcaea官方定数的最小单位) + ) # : 横轴标注以0.1为单位步进 (即arcaea官方定数的最小单位) ax.grid( axis="y", color="r", linestyle="--", linewidth=0.4 ) # 设置图表的外观样式 - if custom_num >= 30: # 生成理论ptt横线,图例自动放在最佳位置 + if custom_num_over30: # 生成理论ptt横线,图例自动放在最佳位置 ax.axhline( y=b30_withr10, linewidth=1, # linewidth linestyle="-.", # linestyle - label=f"不推b30, r10=b10时的理论最高ptt:{b30_withr10:.4f}", + label=f"r10=b10时的理论最高ptt:{b30_withr10:.4f}", ) # 图例 ax.legend(loc="best") # 自动调整图例到最佳位置 ################################################################ @@ -387,12 +370,10 @@ def draw_history_b30_chart(): in_list = xlsx_tolist() # 读入xlsx文件转换成标准list custom_num = cust_input() # 让用户输入想要查看的成绩数量 - desc_list = get_desc_list() # 数据有效性检查, 计算各曲rating - desc_ra_list = desc_list[0] # rating倒序list (单曲ptt) - desc_dt_list = desc_list[1] # detail倒序list (谱面定数) - + desc_ra_list = get_desc_list() # 数据有效性检查, 计算rating返回倒序list cust_average = get_cust_avg() # 根据用户输入的成绩数量计算rating均值 - if custom_num >= 30: # 如果用户输入数量至少为30则: + if custom_num >= 30: + custom_num_over30 = True # 如果用户输入数量至少为30则: b30_pack = get_b30_avg() # 计算b30并return以下两个数据: b30_only = b30_pack[0] # 仅考虑b30底分的ptt b30_withr10 = b30_pack[1] # r10=b10时的理论最高ptt From 1ca13356864072397b4fe9c80955a9ca588cbe8a Mon Sep 17 00:00:00 2001 From: Jacob Liu Date: Fri, 12 Apr 2024 14:55:42 +0800 Subject: [PATCH 11/20] Bugfixes and typos --- b616.py | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/b616.py b/b616.py index 20055db..368be5d 100644 --- a/b616.py +++ b/b616.py @@ -83,7 +83,7 @@ def get_desc_list(): error_row[0], "is", error_row[3], - ", which might be invaild, please check xlsx then run again", + ", which might be invalid, please check xlsx then run again", ) time.sleep(1) input("Press enter to continue") @@ -124,8 +124,6 @@ def write_ptt_history_csv(): # 以下为数据分析呈现函数 - - def show_desc_ra_list(): print() @@ -133,21 +131,21 @@ def print_row(row): print(f"{row[0]} {row[1]} {row[2]} score: {int(row[3])} rating: {row[4]:.4f}") rows = desc_ra_list[:custom_num] - for row in rows[:30]: + for row in rows[:30]: # Print at most 30 rows print_row(row) print() - if custom_num_over30: - print("b30底分:", f"{b30_only:.4f}", " (忽略r10)") - print("r10=b10时的理论最高ptt: ", f"{b30_withr10:.4f}") + if custom_num_over30: # If >= 30 rows, print b30 info + print(f"b30底分: {b30_only:.4f} (忽略r10)") + print(f"r10=b10时的理论最高ptt: {b30_withr10:.4f}") print("---------b30 finished---------") print() - for row in rows[30:]: + for row in rows[30:]: # Print the rest print_row(row) - if custom_num != 30: - print(f"b{custom_num}底分:", f"{cust_average:.4f}", " (忽略r10)") + if custom_num != 30: # No need for repeated b30 + print(f"b{custom_num}底分: {cust_average:.4f} (忽略r10)") print(f"---------b{custom_num} finished---------") @@ -345,19 +343,19 @@ def draw_history_b30_chart(): print("ptt_history数据为空, 跳过生成b30折线图") time.sleep(1) return - x_time = [datetime.strptime(d, "%Y/%m/%d").strftime("%Y/%m/%d") for d in x_time] + x_time = [datetime.strptime(d, "%Y-%m-%d") for d in x_time] dot_size = max((50 - pow(len(x_time), 0.5)), 10) y_realptt = line[1] y_maxiptt = line[2] y_baseptt = line[3] - plt.scatter(x_time, y_realptt, s=dot_size) - plt.scatter(x_time, y_baseptt, s=dot_size) - plt.scatter(x_time, y_maxiptt, s=dot_size) + plt.scatter(x_time, y_realptt, s=dot_size, label="真实ptt") + plt.scatter(x_time, y_baseptt, s=dot_size, label="仅底分ptt") + plt.scatter(x_time, y_maxiptt, s=dot_size, label="理论最高ptt") plt.tick_params(axis="x", labelrotation=61.6) - plt.xlabel("年/月/日", fontsize=12) + plt.xlabel("年-月-日", fontsize=12) plt.ylabel("ptt", fontsize=12) - plt.legend(["真实ptt", "仅底分ptt", "理论最高ptt"], loc="best") + plt.legend(loc="best") plt.show() @@ -369,11 +367,11 @@ def draw_history_b30_chart(): in_list = xlsx_tolist() # 读入xlsx文件转换成标准list custom_num = cust_input() # 让用户输入想要查看的成绩数量 + custom_num_over30 = custom_num >= 30 desc_ra_list = get_desc_list() # 数据有效性检查, 计算rating返回倒序list cust_average = get_cust_avg() # 根据用户输入的成绩数量计算rating均值 - if custom_num >= 30: - custom_num_over30 = True # 如果用户输入数量至少为30则: + if custom_num_over30: b30_pack = get_b30_avg() # 计算b30并return以下两个数据: b30_only = b30_pack[0] # 仅考虑b30底分的ptt b30_withr10 = b30_pack[1] # r10=b10时的理论最高ptt From 9a831f3c61dfe4313f6727604bcf787310a8471a Mon Sep 17 00:00:00 2001 From: Jacob Liu Date: Sat, 13 Apr 2024 23:51:04 +0800 Subject: [PATCH 12/20] Restructure & Start Refractoring b616 Basically a start-over --- .github/workflows/build-all-platforms.yml | 50 -- .gitignore | 4 +- LICENSE | 674 ++++++++++++++++++++ README.md | 30 - b616.py | 388 ----------- ptt_history.csv => b616/__init__.py | 0 b616/core.py | 17 + generate_excel.py => b616/generate_excel.py | 7 +- b616/utils/__init__.py | 0 b616/utils/arcaea_ptt.py | 15 + b616/utils/data_handler.py | 50 ++ b616/utils/plots.py | 72 +++ main.py | 4 + 13 files changed, 837 insertions(+), 474 deletions(-) delete mode 100644 .github/workflows/build-all-platforms.yml create mode 100644 LICENSE delete mode 100644 b616.py rename ptt_history.csv => b616/__init__.py (100%) create mode 100644 b616/core.py rename generate_excel.py => b616/generate_excel.py (97%) create mode 100644 b616/utils/__init__.py create mode 100644 b616/utils/arcaea_ptt.py create mode 100644 b616/utils/data_handler.py create mode 100644 b616/utils/plots.py create mode 100644 main.py diff --git a/.github/workflows/build-all-platforms.yml b/.github/workflows/build-all-platforms.yml deleted file mode 100644 index bd59728..0000000 --- a/.github/workflows/build-all-platforms.yml +++ /dev/null @@ -1,50 +0,0 @@ -# docs at https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python - -name: PyInstaller Build - -on: - workflow_dispatch: - push: - tags: - - v* - -permissions: - contents: read - -jobs: - build: - strategy: - max-parallel: 3 - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - include: - - os: ubuntu-latest - generic-name: Linux - - os: macos-latest - generic-name: MacOS - - os: windows-latest - generic-name: Windows - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - name: Set up Python 3.11 - uses: actions/setup-python@v5 - with: - python-version: "3.11" - cache: "pip" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install pyinstaller - - name: Build with PyInstaller - run: | - pyinstaller --onefile b616.py - pyinstaller --onefile generate_excel.py - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: executables-${{ matrix.generic-name }} - path: dist/* - retention-days: 7 - overwrite: true diff --git a/.gitignore b/.gitignore index 795e432..f6d7042 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,5 @@ .vscode/ -ptt_history.csv -put_your_score_here.xlsx -put_your_score_here_backup.xlsx +scores.xlsx # The rest is taken from https://github.com/github/gitignore/blob/main/Python.gitignore diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index e86fbff..fa1e15e 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,2 @@ # B616 Calculator 根据用户手动管理的excel成绩表生成Arcaea ptt 统计分析的程序. - -## Contributing -`requirements.txt` 包含了运行所有Python脚本所需要的依赖项. -如果打算向项目贡献代码, 请按照 `pip install -r requirements-dev.txt` 安装开发依赖. -安装完开发依赖以后, 请使用 `pre-commit install` 安装本项目使用的pre-commit hooks. -这样每次commit的时候就会自动运行代码格式化和代码检查. - -如果想要使用PyInstaller打包出最小的可执行文件, 请新建一个venv, 并在venv里面打包. - -整套项目在mac和win下应当都通用, 有其他系统测试需求可以在群里提或者提Issue. - -## Usage Instructions -首次使用: - -1. 右键 `ptt_history.csv`, 属性, 打开方式改为'记事本' (或其他plain text editor), -2. 运行 `generate_excel.py`. \*需要网络链接, 请耐心等待. -该程序会在当前目录下创建一个叫做 `put_your_score_here.xlsx` 的文件, -此 `xlsx` 请用 `MS Excel` 打开, 或者其他表格编辑器如 `numbers` 或者 `wps`, 拓展名请固定为 `.xlsx`. -3. 编辑 `put_your_score_here.xlsx`, -在score列输入分数, 无需全都填写, 没打的/不需要参与计算的留空不填就成. -4. 运行 `b616.py`, 第一行输入的数字为参与计算的数据量. -该输入会调整均值计算和图表生成的范围, 不会影响历史ptt输入和推分推荐, 如果小于30则不会计算b30. - - -其他: -1. 如果Arcaea(wiki)有更新, 运行 `generate_excel.py`, 通常能一步到位更新. -运行后请检查 `put_your_score_here.xlsx` , 如果出现数据丢失, 结构错误等问题, 请 *不要* 重复运行程序. -更新前的数据放在同目录下的 `scores_here_backup.xlsx` , 请单独存好并在群里@GK说明 `generate_excel` 错误, 或者提issue. - -2. 如果不小心记录了错误/不需要的历史ptt数据, 可以直接(用记事本)打开 `ptt_history.csv` 修改数据. \ No newline at end of file diff --git a/b616.py b/b616.py deleted file mode 100644 index 368be5d..0000000 --- a/b616.py +++ /dev/null @@ -1,388 +0,0 @@ -import csv -import time -import random -import logging -import pandas as pd -from datetime import datetime -import matplotlib.pyplot as plt -import matplotlib.ticker as ticker - -plt.rc( - "font", family=["Arial", "Microsoft Yahei", "Heiti TC", "sans-serif"] -) # 设定制表使用的字库 - - -# 以下为读取本地数据和用户自定义函数 - - -def xlsx_tolist(): - xlsx = pd.read_excel(r"put_your_score_here.xlsx") - xlsx = xlsx[["title", "label", "detail", "score"]] - xlsx.dropna(inplace=True) # 删除未填入分数的行 credits to GK - return xlsx.values.tolist() - - -def cust_input(): - custom_num = int(input("Hi, B616 here, 请输入显示/计算的成绩行数 e.g.30: ")) - if custom_num <= 0 or custom_num > len(in_list): - print("输入量不符合数据量, 重来") - time.sleep(0.5) - exit() - return custom_num - - -def read_ptt_history_csv(): - with open("ptt_history.csv", mode="r", newline="") as f: - reader = csv.reader(f) - - x_time = [] # x-axis 年月日时间 - y_realptt = [] # y-axis 用户输入的实际ptt - y_baseptt = [] # y-axis 仅b30底分的ptt - y_maxiptt = [] # y-axis r10=b10时的理论最高ptt - for row in reader: - x_time.append(row[0]) - y_realptt.append(float(row[1])) - y_baseptt.append(float(row[2])) - y_maxiptt.append(float(row[3])) - - return x_time, y_realptt, y_baseptt, y_maxiptt - - -# 以下为排序和计算函数 - - -def get_desc_list(): - invalid_score = [] - for row in in_list: - score = row[3] # 玩家输入的得分(score) - detail = row[2] # 谱面的细节定数(detail) - if score >= 10002237 or score < 1002237: - # 不合法的score(2237为1far时score不超过10M的物量阈值) - invalid_score.append(row) - continue - if score >= 10000000: - # 很不幸,你的准度并不会在PM后带来任何rating提升, 准度b歇歇吧 - rating = detail + 2 - row.append(rating) - continue - if score >= 9800000: - # 认认真真的推分哥, salute! 请保持下去, 飞升之路就在脚下 - rating = detail + (score - 9800000) / 200000 + 1 - row.append(rating) - continue - if score >= 1002237: - # 没有ex还想吃分? ex以下单曲rating低得可怜, 能不能推推 - rating = detail + (score - 9500000) / 300000 - rating = max(rating, 0) # 0单曲ra真的有可能出现的, 我猜是你家猫打的( - row.append(rating) - continue - if len(invalid_score) != 0: # 如果有出现不合法的score输入: - for error_row in invalid_score: - print( - "Your score of", - error_row[0], - "is", - error_row[3], - ", which might be invalid, please check xlsx then run again", - ) - time.sleep(1) - input("Press enter to continue") - # 最后根据rating逆向排序并返回 - return sorted(in_list, key=lambda s: s[4], reverse=True) # rating - - -def get_cust_avg(): - ra_sum = 0 - for row in desc_ra_list[0:custom_num]: - ra_sum += row[4] - return ra_sum / custom_num - - -def get_b30_avg(): - b10_sum = 0 - for row in desc_ra_list[0:10]: - b10_sum += row[4] # row[4]为上个方法append的单曲rating值 - restb20_sum = 0 - for row in desc_ra_list[10:30]: - restb20_sum += row[4] - return ( - (restb20_sum + b10_sum) / 30, # 纯b30底分 - (restb20_sum + b10_sum * 2) / 40, # r10=b10时的最高ptt计算公式 - ) - - -def write_ptt_history_csv(): - with open("ptt_history.csv", mode="a", newline="") as f: - line = [ - time.strftime("%Y/%m/%d", time.localtime()), - real_ptt_input, - str(b30_only), - str(b30_withr10), - ] - writer = csv.writer(f) - writer.writerow(line) - - -# 以下为数据分析呈现函数 -def show_desc_ra_list(): - print() - - def print_row(row): - print(f"{row[0]} {row[1]} {row[2]} score: {int(row[3])} rating: {row[4]:.4f}") - - rows = desc_ra_list[:custom_num] - for row in rows[:30]: # Print at most 30 rows - print_row(row) - print() - - if custom_num_over30: # If >= 30 rows, print b30 info - print(f"b30底分: {b30_only:.4f} (忽略r10)") - print(f"r10=b10时的理论最高ptt: {b30_withr10:.4f}") - print("---------b30 finished---------") - print() - - for row in rows[30:]: # Print the rest - print_row(row) - - if custom_num != 30: # No need for repeated b30 - print(f"b{custom_num}底分: {cust_average:.4f} (忽略r10)") - print(f"---------b{custom_num} finished---------") - - -def suggest_song(): - target_rating = desc_ra_list[30][4] # B30th的单曲rating, 超过这个就能推B30底分 - for i in range(min(len(desc_ra_list), 80), 30, -1): - line = desc_ra_list[random.randint(30, i - 1)] - # line=randint的范围会随着循环逐渐从30到min(len(desc_ra_list), 80) - # 每轮上限-1直到最后指定30, 所以接近B30th的分数有更高概率被选中 - detail = line[2] - # 以下是一个比较取巧的判断方式, 通过目标rating和谱面定数之差确认能否推荐(能否满足if/elif) - dt_ra_diff = target_rating - detail - if dt_ra_diff >= 1 and dt_ra_diff < 2: # 说明需要的score在980w到1000w之间 - target_score = 9800000 + (target_rating - detail - 1) * 200000 - title = line[0] - label = line[1] - print( - f"只要把 {title} 的 {label} 难度" - f"推到 {int(target_score)+1} 就可以推b30底分了哦~" - ) - print() - break - elif dt_ra_diff > 0 and dt_ra_diff < 1: # 说明需要的score在950w到980w之间 - target_score = 9500000 + (target_rating - detail) * 300000 - title = line[0] - label = line[1] - print( - f"只要把 {title} 的 {label} 难度" - f"推到 {int(target_score)+1} 就可以推b30底分了哦~" - ) - print() - break - - -def draw_rt_sc_chart(): - sg_title = [] # song title(曲名) - x_detail = [] # x-axis detail(定数) - y_rating = [] # y-axis rating(单曲ptt) - y1_score = [] # y-axis score(单曲得分) - for row in desc_ra_list[0:custom_num]: - sg_title.append(row[0]) - x_detail.append(row[2]) - y1_score.append(row[3]) - y_rating.append(row[4]) - - lx_dt = min(x_detail) - mx_dt = max(x_detail) - ptp_xdt = mx_dt - lx_dt # ptp of numpy (不为这个特地import了) - marksize = max(5, 10 - ptp_xdt - 0.03 * custom_num) # 散点曲名标记的字体大小 - - # 生成 rating/定数 图 - def rating2detail_chart(): - ly_rt = min(y_rating) - my_rt = max(y_rating) - ptp_yrt = my_rt - ly_rt - ################这一段是对图表进行初始化和自定义################ - fig, ax = plt.subplots() - ax.scatter(x_detail, y_rating, s=(20 - pow(custom_num, 0.5))) - ax.set_xlabel("谱面定数", fontsize=12) - ax.set_ylabel("单曲Rating", fontsize=12) - ax.axis( - [lx_dt - 0.01, mx_dt + 0.05, ly_rt - 0.01, my_rt + 0.02] - ) # 设置每个坐标轴的取值范围 - ax.tick_params( - axis="both", which="major", labelright=True, labelsize=10, pad=2, color="r" - ) # 设置刻度标记的样式 - ax.xaxis.set_major_locator( - ticker.MultipleLocator(0.1) - ) # : 横轴标注以0.1为单位步进 (即arcaea官方定数的最小单位) - ax.grid( - axis="y", color="r", linestyle="--", linewidth=0.4 - ) # 设置图表的外观样式 - if custom_num_over30: # 生成理论ptt横线,图例自动放在最佳位置 - ax.axhline( - y=b30_withr10, - linewidth=1, # linewidth - linestyle="-.", # linestyle - label=f"r10=b10时的理论最高ptt:{b30_withr10:.4f}", - ) # 图例 - ax.legend(loc="best") # 自动调整图例到最佳位置 - ################################################################ - - ################这一段是对每个点生成曲名文字标注################### - last_ra = 0 # 上一轮的rating数值 - last_dt = 0 # 上一轮的谱面定数,因为定数不会为0所以第一轮必进else - last_labelen = 0 # 上一轮曲名长度 - for i, label in enumerate(sg_title): - x = x_detail[i] - y = y_rating[i] - if ( - last_ra - y < (0.007 * ptp_yrt) and last_dt == x - ): # 如果跟上一个(同定数的)成绩在y轴距离过近: - extend_len += last_labelen # 根据曲名长度累积的 额外位移距离因数 - extend_counter += 1 # 根据重叠个数累积的 基础位移距离因数 - ax.annotate( - label, - xy=(x, y), - xytext=( - x + extend_counter * ptp_xdt / 32 + extend_len / 120, - y - ptp_yrt / 400, - ), - fontsize=marksize, - ) - else: - extend_len = 0 - extend_counter = 0 - ax.annotate( - label, - xy=(x, y), - xytext=( - x + ptp_xdt / 600, - y - ptp_yrt / 400, - ), - fontsize=marksize, - ) - # 以上的魔数都是为了调整annotation位置, 很抱歉都是试出来的, 但adjust拒绝好好工作所以 - last_ra = y - last_dt = x - last_labelen = len(label) - ################################################################ - plt.show() - - # 生成 score/定数 图 - def score2detail_chart(): - ly_sc = min(y1_score) - my_sc = max(y1_score) - ptp_ysc = my_sc - ly_sc - ################这一段是对图表进行初始化和自定义################### - fig, ax = plt.subplots() - ax.scatter(x_detail, y1_score, s=(20 - pow(custom_num, 0.5))) - ax.set_xlabel("谱面定数", fontsize=12) - ax.set_ylabel("单曲Score", fontsize=12) - ax.axis( - [lx_dt - 0.01, mx_dt + 0.03, ly_sc - 1500, 1e7] - ) # 设置每个坐标轴的取值范围, Y轴最高固定取10M(即PM线) - ax.tick_params( - axis="both", which="major", labelright=True, labelsize=10, pad=2, color="r" - ) # 设置刻度标记的样式 - ax.xaxis.set_major_locator( - ticker.MultipleLocator(0.1) - ) # tick_spacing = 0.1: 横轴标注以0.1为单位步进 - ax.grid( - axis="y", color="r", linestyle="--", linewidth=0.4 - ) # 设置图表的外观样式 - ################################################################ - - ################这一段是对每个点生成曲名文字标注################### - last_sc = 0 # 上一轮的score数值 - last_dt = 0 # 上一轮的谱面定数 - last_labelen = 0 # 上一轮曲名长度 - for i, label in enumerate(sg_title): - x = x_detail[i] - y = y1_score[i] - if ( - last_sc - y < (0.01 * ptp_ysc) and last_dt == x - ): # 如果两个同定数的成绩y轴距离过近: - extend_len += last_labelen - extend_counter += 1 - ax.annotate( - label, - xy=(x, y), - xytext=( - x + extend_counter * ptp_xdt / 32 + extend_len / 120, - y - ptp_ysc / 400, - ), - fontsize=marksize, - ) - else: - extend_len = 0 - extend_counter = 0 - ax.annotate( - label, - xy=(x, y), - xytext=( - x + ptp_xdt / 600, - y - ptp_ysc / 400, - ), - fontsize=marksize, - ) - # 以上的魔数都是为了调整annotation位置 - last_sc = y - last_dt = x - last_labelen = len(label) - ################################################################ - plt.show() - - rating2detail_chart() - score2detail_chart() - - -def draw_history_b30_chart(): - line = read_ptt_history_csv() # 打开csv并读取用户过去填入的数据 - x_time = line[0] - if len(x_time) == 0: - print("ptt_history数据为空, 跳过生成b30折线图") - time.sleep(1) - return - x_time = [datetime.strptime(d, "%Y-%m-%d") for d in x_time] - dot_size = max((50 - pow(len(x_time), 0.5)), 10) - - y_realptt = line[1] - y_maxiptt = line[2] - y_baseptt = line[3] - plt.scatter(x_time, y_realptt, s=dot_size, label="真实ptt") - plt.scatter(x_time, y_baseptt, s=dot_size, label="仅底分ptt") - plt.scatter(x_time, y_maxiptt, s=dot_size, label="理论最高ptt") - plt.tick_params(axis="x", labelrotation=61.6) - plt.xlabel("年-月-日", fontsize=12) - plt.ylabel("ptt", fontsize=12) - plt.legend(loc="best") - plt.show() - - -if __name__ == "__main__": - - logging.getLogger("matplotlib.font_manager").setLevel( - logging.ERROR - ) # 忽略字体Error级以下的报错 - - in_list = xlsx_tolist() # 读入xlsx文件转换成标准list - custom_num = cust_input() # 让用户输入想要查看的成绩数量 - custom_num_over30 = custom_num >= 30 - - desc_ra_list = get_desc_list() # 数据有效性检查, 计算rating返回倒序list - cust_average = get_cust_avg() # 根据用户输入的成绩数量计算rating均值 - if custom_num_over30: - b30_pack = get_b30_avg() # 计算b30并return以下两个数据: - b30_only = b30_pack[0] # 仅考虑b30底分的ptt - b30_withr10 = b30_pack[1] # r10=b10时的理论最高ptt - - real_ptt_input = input("请输入当前您的实际ptt(例 12.47): ") - if input("是否要更新历史ptt数据(Y/N): ").upper() == "Y": # by和Y都会确认 - write_ptt_history_csv() # 把 real_ptt_input和b30_withr10 存档, 用来生成变化图像 - print() - - show_desc_ra_list() # 展示根据rating排序的分数列表 - if len(desc_ra_list) > 30: # 如果有超过30行数据则: - suggest_song() # 尝试给用户推荐一个能替换b30th的谱面 - draw_rt_sc_chart() # 展示rating or score/detail关系图 - draw_history_b30_chart() # 展示b30/time, ptt变化记录 diff --git a/ptt_history.csv b/b616/__init__.py similarity index 100% rename from ptt_history.csv rename to b616/__init__.py diff --git a/b616/core.py b/b616/core.py new file mode 100644 index 0000000..edb37f4 --- /dev/null +++ b/b616/core.py @@ -0,0 +1,17 @@ +from b616.utils.data_handler import DataHandler +from b616.utils import plots + +import matplotlib.pyplot as plt + + +def main(): + # Set font library to support Chinese characters + plt.rc("font", family=plots.get_available_fonts()) + + maxlines = int(input("Please input the number of datapoints to analyse: ")) + data_handler = DataHandler.from_xlsx("put_your_score_here.xlsx", maxlines=maxlines) + + plots.ptt_against_chartconstant(data_handler) + plots.score_against_chartconstant(data_handler) + + plt.show() diff --git a/generate_excel.py b/b616/generate_excel.py similarity index 97% rename from generate_excel.py rename to b616/generate_excel.py index a2cb038..772c0dc 100644 --- a/generate_excel.py +++ b/b616/generate_excel.py @@ -27,6 +27,8 @@ def preprocess_songlist(raw): songlist = preprocess_songlist(songlist) transition = requests.get(URLTEMPLATE.format("Template:Transition.json")).json() +print("Done fetching data") + def disambiguate_name(name: str, songid: str) -> str: if name in transition["sameName"]: @@ -50,7 +52,7 @@ def get_detail_for_sorting(difficulty_record) -> float: return base_difficulty -def get_all_entries() -> pd.DataFrame: +def get_all_entries(): rows = [] for songid, song_info in songlist.items(): for difficulty_record in song_info["difficulties"]: @@ -110,9 +112,8 @@ def main(): df_output = df df_output["score"] = pd.NA warnings.warn( - "Unable to process old scores, continuing without them", UserWarning + f"Unable to process old scores, continuing without them. Error: {e}" ) - print(e) df_output.reset_index(inplace=True) diff --git a/b616/utils/__init__.py b/b616/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/b616/utils/arcaea_ptt.py b/b616/utils/arcaea_ptt.py new file mode 100644 index 0000000..c433e22 --- /dev/null +++ b/b616/utils/arcaea_ptt.py @@ -0,0 +1,15 @@ +import numpy as np + +_THRESHOLDS = { + "score": [9_500_000 - 12 * 300_000, 9_500_000, 9_800_000, 10_000_000], + "ptt_delta": [-12, 0, 1, 2], +} + + +def get_ptt_delta(score): + return np.interp(score, _THRESHOLDS["score"], _THRESHOLDS["ptt_delta"]) + + +def get_score(ptt_delta): + # NOTE: will never return a score less than 9_500_000 - 12 * 300_000 == 5_900_000 + return np.interp(ptt_delta, _THRESHOLDS["ptt_delta"], _THRESHOLDS["score"]) diff --git a/b616/utils/data_handler.py b/b616/utils/data_handler.py new file mode 100644 index 0000000..5a583cc --- /dev/null +++ b/b616/utils/data_handler.py @@ -0,0 +1,50 @@ +import pandas as pd +from functools import cache +from b616.utils.arcaea_ptt import get_ptt_delta + + +class DataHandler: + """A class to handle Arcaea score data. + + Note that the data is immutable after initialization. + If you want to modify the data, you should create a new DataHandler object. + This enables caching of some computationally expensive methods. + """ + + def __init__(self, data: pd.DataFrame, *, maxlines: int | None = None) -> None: + """ + Expects a DataFrame with columns + "title", "difficulty", "chart_constant", "score". + """ + self._data = data.copy() + self._data.dropna(inplace=True) + self._data["ptt"] = get_ptt_delta(self._data["score"]) + self._data["ptt"] += self._data["chart_constant"] + + self._data.sort_values(by="ptt", ascending=False, inplace=True) + + if maxlines is not None: + self._data = self._data.head(maxlines) + + def get_data(self) -> pd.DataFrame: + """Return a *copy* of the data.""" + return self._data.copy() + + def get_column(self, column: str) -> pd.Series: + """Return a *copy* of a specific column.""" + return self._data[column].copy() + + def get_best_n(self, n: int) -> pd.DataFrame: + """Return the best-ptt n song entries (copied).""" + return self._data.head(n).copy() + + @cache + def get_best_n_pttavg(self, n: int = 30) -> float: + """Return the average PTT of the best n scores.""" + return self._data.head(n)["ptt"].mean() + + @classmethod + def from_xlsx(cls, path: str, *args, **kwargs) -> "DataHandler": + data = pd.read_excel(path) + data = data.rename(columns={"label": "difficulty", "detail": "chart_constant"}) + return cls(data, *args, **kwargs) diff --git a/b616/utils/plots.py b/b616/utils/plots.py new file mode 100644 index 0000000..8e4da59 --- /dev/null +++ b/b616/utils/plots.py @@ -0,0 +1,72 @@ +import matplotlib.pyplot as plt +import matplotlib.font_manager as fm +from b616.utils.data_handler import DataHandler + +_FONT_CANDIDATES = ["Arial", "Microsoft YaHei", "HeiTi TC"] + + +def get_available_fonts(): + available_fonts = set(fm.get_font_names()) + + usefonts = [font for font in _FONT_CANDIDATES if font in available_fonts] + usefonts.append("sans-serif") + + return usefonts + + +def _add_hover_annotation(fig, ax, artist, text_from_ind): + def hover(event): + if event.inaxes != ax: + return + cont, ind = artist.contains(event) + if not cont: + hover_annotation.set_visible(False) + fig.canvas.draw_idle() + return + ind = ind["ind"][0] + x, y = artist.get_offsets()[ind] + hover_annotation.xy = (x, y) + hover_annotation.set_text(text_from_ind(ind)) + hover_annotation.set_visible(True) + fig.canvas.draw_idle() + + hover_annotation = ax.annotate( + "", + xy=(0, 0), + xytext=(10, 10), + textcoords="offset points", + bbox={"fc": "w"}, + arrowprops={"arrowstyle": "->"}, + visible=False, + ) + fig.canvas.mpl_connect("motion_notify_event", hover) + + +def ptt_against_chartconstant(data_handler: DataHandler): + x = data_handler.get_column("chart_constant") + y = data_handler.get_column("ptt") + titles = data_handler.get_column("title") + + fig, ax = plt.subplots() + + scatter = ax.scatter(x, y) + ax.set_xlabel("Chart Constant") + ax.set_ylabel("Song PTT") + ax.set_title("PTT against Chart Constant") + _add_hover_annotation(fig, ax, scatter, lambda ind: titles.iloc[ind]) + return fig + + +def score_against_chartconstant(data_handler: DataHandler): + x = data_handler.get_column("chart_constant") + y = data_handler.get_column("score") + titles = data_handler.get_column("title") + + fig, ax = plt.subplots() + + scatter = ax.scatter(x, y) + ax.set_xlabel("Chart Constant") + ax.set_ylabel("Score") + ax.set_title("Score against Chart Constant") + _add_hover_annotation(fig, ax, scatter, lambda ind: titles.iloc[ind]) + return fig diff --git a/main.py b/main.py new file mode 100644 index 0000000..76b5898 --- /dev/null +++ b/main.py @@ -0,0 +1,4 @@ +from b616 import core + +if __name__ == "__main__": + core.main() From b6e0bd478efcaf193d19c93bb40f04d1d4141158 Mon Sep 17 00:00:00 2001 From: Jacob Liu Date: Sat, 13 Apr 2024 23:54:42 +0800 Subject: [PATCH 13/20] Change scores filename to `scores.xlsx` --- b616/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b616/core.py b/b616/core.py index edb37f4..e618694 100644 --- a/b616/core.py +++ b/b616/core.py @@ -9,7 +9,7 @@ def main(): plt.rc("font", family=plots.get_available_fonts()) maxlines = int(input("Please input the number of datapoints to analyse: ")) - data_handler = DataHandler.from_xlsx("put_your_score_here.xlsx", maxlines=maxlines) + data_handler = DataHandler.from_xlsx("scores.xlsx", maxlines=maxlines) plots.ptt_against_chartconstant(data_handler) plots.score_against_chartconstant(data_handler) From 6356fe946fa93e8c5b5a60795809725ff3530bcf Mon Sep 17 00:00:00 2001 From: Jacob Liu Date: Sun, 14 Apr 2024 00:29:15 +0800 Subject: [PATCH 14/20] Add build venv to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index f6d7042..da88fb6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ .vscode/ scores.xlsx +# Since we're using PyInstaller a separate build venv is needed +build_venv/ # The rest is taken from https://github.com/github/gitignore/blob/main/Python.gitignore # Byte-compiled / optimized / DLL files From 14453ce79392e8edef27652034b9de877c90bdf0 Mon Sep 17 00:00:00 2001 From: Jacob Liu Date: Sun, 14 Apr 2024 00:29:33 +0800 Subject: [PATCH 15/20] Enable Pandas copy-on-write --- b616/core.py | 7 ++++++- b616/utils/data_handler.py | 11 +++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/b616/core.py b/b616/core.py index e618694..c581a3e 100644 --- a/b616/core.py +++ b/b616/core.py @@ -1,7 +1,12 @@ +import pandas as pd +import matplotlib.pyplot as plt + from b616.utils.data_handler import DataHandler from b616.utils import plots -import matplotlib.pyplot as plt +# As recommended by the pandas documentation, we enable copy-on-write mode +# This improves DataHandler's performance since it returns copies of the data +pd.set_option("mode.copy_on_write", True) def main(): diff --git a/b616/utils/data_handler.py b/b616/utils/data_handler.py index 5a583cc..a97c688 100644 --- a/b616/utils/data_handler.py +++ b/b616/utils/data_handler.py @@ -9,6 +9,13 @@ class DataHandler: Note that the data is immutable after initialization. If you want to modify the data, you should create a new DataHandler object. This enables caching of some computationally expensive methods. + + The data returned is sorted by PTT in descending order. + + NOTE: All returned data are copies of the original data, so they are safe to modify. + Using Copy-On-Write mode in pandas is strongly recommended for performance reasons. + (Also it's recommended by the pandas documentation.) + https://pandas.pydata.org/docs/user_guide/copy_on_write.html """ def __init__(self, data: pd.DataFrame, *, maxlines: int | None = None) -> None: @@ -27,11 +34,11 @@ def __init__(self, data: pd.DataFrame, *, maxlines: int | None = None) -> None: self._data = self._data.head(maxlines) def get_data(self) -> pd.DataFrame: - """Return a *copy* of the data.""" + """Return a *copy* of the data sorted by PTT (descending).""" return self._data.copy() def get_column(self, column: str) -> pd.Series: - """Return a *copy* of a specific column.""" + """Return a *copy* of a specific column sorted by PTT (descending).""" return self._data[column].copy() def get_best_n(self, n: int) -> pd.DataFrame: From 1f3a160ed9a80ec10963506241e2b7cd4a615f92 Mon Sep 17 00:00:00 2001 From: Jacob Liu Date: Sun, 14 Apr 2024 00:47:33 +0800 Subject: [PATCH 16/20] More extensively use copy-on-write. Correctness still holds if copy-on-write is off. --- b616/utils/data_handler.py | 24 +++++++++++------------- b616/utils/plots.py | 12 ++++++------ 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/b616/utils/data_handler.py b/b616/utils/data_handler.py index a97c688..707d636 100644 --- a/b616/utils/data_handler.py +++ b/b616/utils/data_handler.py @@ -5,14 +5,16 @@ class DataHandler: """A class to handle Arcaea score data. - - Note that the data is immutable after initialization. - If you want to modify the data, you should create a new DataHandler object. - This enables caching of some computationally expensive methods. - The data returned is sorted by PTT in descending order. + Note that the data shall not be mutated after initialization. + If you want to modify the data, you should get the data, modify it, + and then create a new DataHandler object with the modified data. + NOTE: All returned data are copies of the original data, so they are safe to modify. + This class is coded with the assumption that copy-on-write mode is enabled. + Correctness probably still holds without copy-on-write mode, but performance may be worse. + Using Copy-On-Write mode in pandas is strongly recommended for performance reasons. (Also it's recommended by the pandas documentation.) https://pandas.pydata.org/docs/user_guide/copy_on_write.html @@ -23,8 +25,7 @@ def __init__(self, data: pd.DataFrame, *, maxlines: int | None = None) -> None: Expects a DataFrame with columns "title", "difficulty", "chart_constant", "score". """ - self._data = data.copy() - self._data.dropna(inplace=True) + self._data = data.dropna().copy() self._data["ptt"] = get_ptt_delta(self._data["score"]) self._data["ptt"] += self._data["chart_constant"] @@ -33,14 +34,11 @@ def __init__(self, data: pd.DataFrame, *, maxlines: int | None = None) -> None: if maxlines is not None: self._data = self._data.head(maxlines) - def get_data(self) -> pd.DataFrame: - """Return a *copy* of the data sorted by PTT (descending).""" + @property + def data(self) -> pd.DataFrame: + """Return the data (copied).""" return self._data.copy() - def get_column(self, column: str) -> pd.Series: - """Return a *copy* of a specific column sorted by PTT (descending).""" - return self._data[column].copy() - def get_best_n(self, n: int) -> pd.DataFrame: """Return the best-ptt n song entries (copied).""" return self._data.head(n).copy() diff --git a/b616/utils/plots.py b/b616/utils/plots.py index 8e4da59..11a1316 100644 --- a/b616/utils/plots.py +++ b/b616/utils/plots.py @@ -43,9 +43,9 @@ def hover(event): def ptt_against_chartconstant(data_handler: DataHandler): - x = data_handler.get_column("chart_constant") - y = data_handler.get_column("ptt") - titles = data_handler.get_column("title") + x = data_handler.data["chart_constant"] + y = data_handler.data["ptt"] + titles = data_handler.data["title"] fig, ax = plt.subplots() @@ -58,9 +58,9 @@ def ptt_against_chartconstant(data_handler: DataHandler): def score_against_chartconstant(data_handler: DataHandler): - x = data_handler.get_column("chart_constant") - y = data_handler.get_column("score") - titles = data_handler.get_column("title") + x = data_handler.data["chart_constant"] + y = data_handler.data["score"] + titles = data_handler.data["title"] fig, ax = plt.subplots() From fedc3cb3361611083a222b982b66d400f6f8e2ac Mon Sep 17 00:00:00 2001 From: Jacob Liu Date: Sun, 14 Apr 2024 15:53:13 +0800 Subject: [PATCH 17/20] Prettier ptt-against-chartconstant --- b616/utils/plots.py | 44 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/b616/utils/plots.py b/b616/utils/plots.py index 11a1316..51441d6 100644 --- a/b616/utils/plots.py +++ b/b616/utils/plots.py @@ -1,8 +1,23 @@ import matplotlib.pyplot as plt +import numpy as np import matplotlib.font_manager as fm +from matplotlib import colors + from b616.utils.data_handler import DataHandler +from b616.utils.arcaea_ptt import get_ptt_delta _FONT_CANDIDATES = ["Arial", "Microsoft YaHei", "HeiTi TC"] +_SCORE_THRESHOLDS_NAMES: list[tuple[str, int]] = [ + ("PM", 10_000_000), + ("EX+", 9_900_000), + ("EX", 9_800_000), + ("AA", 9_500_000), + ("A", 9_200_000), +] + +_score_colormap = plt.cm.get_cmap("plasma_r") +_score_norm = colors.Normalize(vmin=9_000_000, vmax=10_000_000, clip=True) +_score_scalarmappable = plt.cm.ScalarMappable(norm=_score_norm, cmap=_score_colormap) def get_available_fonts(): @@ -46,14 +61,39 @@ def ptt_against_chartconstant(data_handler: DataHandler): x = data_handler.data["chart_constant"] y = data_handler.data["ptt"] titles = data_handler.data["title"] + scores = data_handler.data["score"] fig, ax = plt.subplots() - scatter = ax.scatter(x, y) + scatter = ax.scatter(x, y, s=15, c=_score_norm(scores), cmap=_score_colormap) + fig.draw_without_rendering() + ax.autoscale(False) # Fix the axes so that the line can be drawn + + line_x = np.array([0, 15]) + min_score = scores.min() + for threshold_name, threshold in _SCORE_THRESHOLDS_NAMES: + line_y = get_ptt_delta(threshold) + line_x + ax.plot( + line_x, + line_y, + label=f"{threshold_name}", + linestyle="--", + linewidth=1, + alpha=0.3, + color=_score_colormap(_score_norm(threshold)), + ) + + if min_score > threshold: + break + ax.set_xlabel("Chart Constant") + ax.xaxis.set_minor_locator(plt.MultipleLocator(0.1)) + ax.xaxis.set_major_locator(plt.MultipleLocator(0.2)) ax.set_ylabel("Song PTT") ax.set_title("PTT against Chart Constant") - _add_hover_annotation(fig, ax, scatter, lambda ind: titles.iloc[ind]) + ax.legend() + fig.colorbar(_score_scalarmappable, ax=ax, extend="both") + # _add_hover_annotation(fig, ax, scatter, lambda ind: titles.iloc[ind]) return fig From 2cb16f3ffab383cb970f99c527b0b99639ffdabe Mon Sep 17 00:00:00 2001 From: Jacob Liu Date: Sun, 14 Apr 2024 16:31:43 +0800 Subject: [PATCH 18/20] More aggressive color gradient --- b616/utils/plots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b616/utils/plots.py b/b616/utils/plots.py index 51441d6..7014d9e 100644 --- a/b616/utils/plots.py +++ b/b616/utils/plots.py @@ -16,7 +16,7 @@ ] _score_colormap = plt.cm.get_cmap("plasma_r") -_score_norm = colors.Normalize(vmin=9_000_000, vmax=10_000_000, clip=True) +_score_norm = colors.Normalize(vmin=9_500_000, vmax=10_000_000, clip=True) _score_scalarmappable = plt.cm.ScalarMappable(norm=_score_norm, cmap=_score_colormap) From 16d3d92b815f282f4ca84691faa3217ef1e29450 Mon Sep 17 00:00:00 2001 From: Jacob Liu Date: Mon, 15 Apr 2024 22:44:12 +0800 Subject: [PATCH 19/20] Toggle-able song name annotations --- b616/utils/plots.py | 103 ++++++++++++++++++++++++++++++++------------ 1 file changed, 75 insertions(+), 28 deletions(-) diff --git a/b616/utils/plots.py b/b616/utils/plots.py index 7014d9e..2b0fa0a 100644 --- a/b616/utils/plots.py +++ b/b616/utils/plots.py @@ -1,7 +1,9 @@ import matplotlib.pyplot as plt import numpy as np +import pandas as pd import matplotlib.font_manager as fm from matplotlib import colors +from matplotlib.widgets import Button from b616.utils.data_handler import DataHandler from b616.utils.arcaea_ptt import get_ptt_delta @@ -15,6 +17,11 @@ ("A", 9_200_000), ] +# Prevent the garbage collector from collecting the objects +# This is necessary because Matplotlib widgets need to be kept alive by the user +_keepalive: list = [] + +# colormap _score_colormap = plt.cm.get_cmap("plasma_r") _score_norm = colors.Normalize(vmin=9_500_000, vmax=10_000_000, clip=True) _score_scalarmappable = plt.cm.ScalarMappable(norm=_score_norm, cmap=_score_colormap) @@ -29,32 +36,68 @@ def get_available_fonts(): return usefonts -def _add_hover_annotation(fig, ax, artist, text_from_ind): - def hover(event): - if event.inaxes != ax: - return - cont, ind = artist.contains(event) - if not cont: - hover_annotation.set_visible(False) - fig.canvas.draw_idle() - return - ind = ind["ind"][0] - x, y = artist.get_offsets()[ind] - hover_annotation.xy = (x, y) - hover_annotation.set_text(text_from_ind(ind)) - hover_annotation.set_visible(True) +def add_toggleable_annotations(xs, ys, texts, fig, ax, artist): + DEFAULT_OFFSET = (5, 0) + annotations_by_x = dict() # map x to list of annotations + all_annotations = [] # flat list of all annotations, for indexing when toggling + for x, y, text in zip(xs, ys, texts): + annotation = ax.annotate( + text, + (x, y), + textcoords="offset pixels", + xytext=DEFAULT_OFFSET, + horizontalalignment="left", + verticalalignment="baseline", + fontsize=8, + color="black", + bbox={"facecolor": "white", "alpha": 0.2, "edgecolor": "none", "pad": 1}, + ) + annotations_by_x.setdefault(x, []).append(annotation) + all_annotations.append(annotation) + + def adjust_positions(): + fig.draw_without_rendering() + for annotations in annotations_by_x.values(): + # Annotations are already sorted by y, because the data is sorted by PTT + # So for any given chart constant, scores are in descending order + for annotation, prev_annotation in zip(annotations[1:], annotations[:-1]): + annotation.set_position(DEFAULT_OFFSET) + bbox = annotation.get_window_extent() + this_y_top = bbox.bounds[1] + bbox.bounds[3] + prev_y = prev_annotation.get_tightbbox().bounds[1] + + if this_y_top > prev_y: + delta = this_y_top - prev_y + xtext, ytext = annotation.get_position() + annotation.set_position((xtext, ytext - delta - 5)) + fig.canvas.draw_idle() + + fig.canvas.mpl_connect("resize_event", lambda _: adjust_positions()) + ax.callbacks.connect("ylim_changed", lambda _: adjust_positions()) + + def set_all_visibility(visible): + for annotation in all_annotations: + annotation.set_visible(visible) + fig.canvas.draw_idle() + + ax_select_all = fig.add_axes([0.65, 0.01, 0.14, 0.05]) + button_show_all = Button(ax_select_all, "Show All") + button_show_all.on_clicked(lambda _: set_all_visibility(True)) + ax_deselect_all = fig.add_axes([0.8, 0.01, 0.14, 0.05]) + button_hide_all = Button(ax_deselect_all, "Hide All") + button_hide_all.on_clicked(lambda _: set_all_visibility(False)) + # Keep the buttons alive so that they are not garbage collected + _keepalive.extend([button_show_all, button_hide_all]) + + artist.set_picker(5) + + def on_pick(event): + ind = event.ind[0] + visibility = not all_annotations[ind].get_visible() + all_annotations[ind].set_visible(visibility) fig.canvas.draw_idle() - hover_annotation = ax.annotate( - "", - xy=(0, 0), - xytext=(10, 10), - textcoords="offset points", - bbox={"fc": "w"}, - arrowprops={"arrowstyle": "->"}, - visible=False, - ) - fig.canvas.mpl_connect("motion_notify_event", hover) + fig.canvas.mpl_connect("pick_event", on_pick) def ptt_against_chartconstant(data_handler: DataHandler): @@ -66,9 +109,10 @@ def ptt_against_chartconstant(data_handler: DataHandler): fig, ax = plt.subplots() scatter = ax.scatter(x, y, s=15, c=_score_norm(scores), cmap=_score_colormap) + + # Draw the PTT lines for each score threshold fig.draw_without_rendering() ax.autoscale(False) # Fix the axes so that the line can be drawn - line_x = np.array([0, 15]) min_score = scores.min() for threshold_name, threshold in _SCORE_THRESHOLDS_NAMES: @@ -93,7 +137,8 @@ def ptt_against_chartconstant(data_handler: DataHandler): ax.set_title("PTT against Chart Constant") ax.legend() fig.colorbar(_score_scalarmappable, ax=ax, extend="both") - # _add_hover_annotation(fig, ax, scatter, lambda ind: titles.iloc[ind]) + + add_toggleable_annotations(x, y, titles, fig, ax, scatter) return fig @@ -101,12 +146,14 @@ def score_against_chartconstant(data_handler: DataHandler): x = data_handler.data["chart_constant"] y = data_handler.data["score"] titles = data_handler.data["title"] + scores = data_handler.data["score"] fig, ax = plt.subplots() - scatter = ax.scatter(x, y) + scatter = ax.scatter(x, y, s=15, c=_score_norm(scores), cmap=_score_colormap) ax.set_xlabel("Chart Constant") ax.set_ylabel("Score") ax.set_title("Score against Chart Constant") - _add_hover_annotation(fig, ax, scatter, lambda ind: titles.iloc[ind]) + + add_toggleable_annotations(x, y, titles, fig, ax, scatter) return fig From 343cdfe1144f85f8525a8cf347fed3d6baabf111 Mon Sep 17 00:00:00 2001 From: Jacob Liu Date: Mon, 15 Apr 2024 22:51:03 +0800 Subject: [PATCH 20/20] Fix mixed line ending for pyinstaller build --- .github/workflows/pyinstaller-build.yml | 47 +++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/pyinstaller-build.yml diff --git a/.github/workflows/pyinstaller-build.yml b/.github/workflows/pyinstaller-build.yml new file mode 100644 index 0000000..e037f0e --- /dev/null +++ b/.github/workflows/pyinstaller-build.yml @@ -0,0 +1,47 @@ +# docs at https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: PyInstaller Build + +on: + workflow_dispatch: + push: + tags: + - v* + +permissions: + contents: read + +jobs: + build: + strategy: + max-parallel: 3 + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + include: + - os: ubuntu-latest + generic-name: Linux + - os: macos-latest + generic-name: MacOS + - os: windows-latest + generic-name: Windows + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: "pip" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pyinstaller + - name: Build with PyInstaller + run: | + pyinstaller --onefile main.py + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: executables-${{ matrix.generic-name }} + path: dist/*