libs.commands.results.detail

libs/commands/results/detail.py

  1"""
  2libs/commands/results/detail.py
  3"""
  4
  5import textwrap
  6from typing import TYPE_CHECKING, Any
  7
  8import numpy as np
  9import pandas as pd
 10from table2ascii import Alignment, PresetStyle, table2ascii
 11
 12import libs.global_value as g
 13from libs.domain.datamodels import GameInfo
 14from libs.domain.stats import StatsInfo
 15from libs.functions import message
 16from libs.functions.compose import badge, text_item
 17from libs.types import StyleOptions
 18from libs.utils import converter, formatter
 19
 20if TYPE_CHECKING:
 21    from integrations.protocols import MessageParserProtocol
 22    from libs.types import MessageType
 23
 24
 25def aggregation(m: "MessageParserProtocol") -> None:
 26    """
 27    成績詳細を集計
 28
 29    Args:
 30        m (MessageParserProtocol): メッセージデータ
 31
 32    """
 33    # --- パラメータ更新
 34    g.params.guest_skip = g.params.guest_skip2  # 検索動作を合わせる
 35
 36    if rule_version := g.params.rule_version:
 37        g.params.update_from_dict(
 38            {
 39                "mode": int(g.cfg.rule.to_dict(rule_version).get("mode", 4)),
 40                "rule_version": str(g.cfg.rule.to_dict(rule_version).get("rule_version", "")),
 41                "origin_point": int(g.cfg.rule.to_dict(rule_version).get("origin_point", 250)),
 42                "return_point": int(g.cfg.rule.to_dict(rule_version).get("return_point", 300)),
 43            }
 44        )
 45        if (target_mode := g.params.target_mode) and target_mode != g.cfg.rule.get_mode(rule_version):
 46            m.set_headline(message.random_reply(m, "rule_mismatch"), StyleOptions(title="集計矛盾検出"))
 47            m.status.result = False
 48            return
 49    if g.params.player_name in g.cfg.team.lists:
 50        g.params.individual = False
 51    elif g.params.player_name in g.cfg.member.lists:
 52        g.params.individual = True
 53
 54    # --- データ収集
 55    game_info = GameInfo()
 56    msg_data: dict[str, str] = {}
 57    mapping_dict: dict[str, str] = {}
 58
 59    # タイトル
 60    if g.params.individual:
 61        title = "個人成績詳細"
 62    else:
 63        title = "チーム成績詳細"
 64
 65    if game_info.count == 0:
 66        if g.params.individual:
 67            msg_data["検索範囲"] = f"{text_item.search_range(time_pattern='time')}"
 68            msg_data["特記事項"] = "、".join(text_item.remarks())
 69            msg_data["検索ワード"] = text_item.search_word()
 70            msg_data["対戦数"] = f"0 戦 (0 勝 0 敗 0 分) {badge.status(0, 0)}"
 71            m.set_headline(message_build(msg_data), StyleOptions(title=title))
 72        else:
 73            m.set_headline("登録されていないチームです。", StyleOptions(title=title))
 74        m.status.result = False
 75        return
 76
 77    stats = StatsInfo()
 78    stats.read(g.params)
 79
 80    if stats.result_df.empty or stats.record_df.empty:
 81        m.set_headline(message.random_reply(m, "no_target"), StyleOptions(title=title))
 82        m.status.result = False
 83        return
 84
 85    player_name = formatter.name_replace(g.params.player_name, add_mark=True)
 86    if g.params.anonymous:
 87        mapping_dict = formatter.anonymous_mapping(stats.result_df["name"].unique().tolist())
 88        stats.result_df["name"] = stats.result_df["name"].replace(mapping_dict)
 89        player_name = mapping_dict[player_name]
 90
 91    # --- 表示内容
 92    msg_data.update(get_headline(stats, game_info, player_name))
 93    msg_data.update(get_totalization(stats))
 94    mode = g.params.mode
 95
 96    # 統計
 97    seat_data = pd.DataFrame(
 98        {  # 座席データ
 99            "席": ["東家", "南家", "西家", "北家"][:mode],
100            "順位分布": stats.rank_distr_list2,
101            "平均順位": [f"{x:.2f}".replace("0.00", "-.--") for x in stats.rank_avg_list],
102            "トビ": stats.flying_list,
103            "役満和了": stats.yakuman_list,
104        }
105    )
106
107    if g.params.ignore_flying or g.cfg.rule.dropitems(g.params.rule_version) & g.cfg.dropitems.flying:
108        seat_data.drop(columns=["トビ"], inplace=True)
109    if g.cfg.rule.dropitems(g.params.rule_version) & g.cfg.dropitems.yakuman:
110        seat_data.drop(columns=["役満和了"], inplace=True)
111
112    if mode == 3:
113        balance_data = textwrap.dedent(
114            f"""\
115            全体:{stats.seat0.avg_balance("all"):+.1f}
116            1着終了時:{stats.seat0.avg_balance("rank1"):+.1f}
117            2着終了時:{stats.seat0.avg_balance("rank2"):+.1f}
118            3着終了時:{stats.seat0.avg_balance("rank3"):+.1f}
119            """.replace("+0.0点", "記録なし")
120        ).replace("-", "▲")
121    else:
122        balance_data = textwrap.dedent(
123            f"""\
124            全体:{stats.seat0.avg_balance("all"):+.1f}
125            連対時:{stats.seat0.avg_balance("top2"):+.1f}
126            逆連対時:{stats.seat0.avg_balance("lose2"):+.1f}
127            1着終了時:{stats.seat0.avg_balance("rank1"):+.1f}
128            2着終了時:{stats.seat0.avg_balance("rank2"):+.1f}
129            3着終了時:{stats.seat0.avg_balance("rank3"):+.1f}
130            4着終了時:{stats.seat0.avg_balance("rank4"):+.1f}
131            """.replace("+0.0点", "記録なし")
132        ).replace("-", "▲")
133
134    if g.params.statistics:
135        m.set_message(seat_data, StyleOptions(title="座席データ", data_kind=StyleOptions.DataKind.SEAT_DATA))
136        m.set_message(textwrap.indent(stats.seat0.best_record(), "\t"), StyleOptions(title="ベストレコード"))
137        m.set_message(textwrap.indent(stats.seat0.worst_record(), "\t"), StyleOptions(title="ワーストレコード"))
138        m.set_message(textwrap.indent(balance_data.strip(), "\t"), StyleOptions(title="平均収支"))
139
140    # レギュレーション
141    remarks_df = g.params.read_data("REMARKS_INFO")
142    count_df = remarks_df.groupby("matter").agg(matter_count=("matter", "count"), ex_total=("ex_point", "sum"), type=("type", "max"))
143    count_df["matter"] = count_df.index
144
145    if not g.cfg.rule.dropitems(g.params.rule_version) & g.cfg.dropitems.yakuman:
146        work_df = count_df.query("type == 0").filter(items=["matter", "matter_count"])
147        m.set_message(work_df, StyleOptions(title="役満和了", data_kind=StyleOptions.DataKind.REMARKS_YAKUMAN))
148
149    if not g.cfg.rule.dropitems(g.params.rule_version) & g.cfg.dropitems.regulation:
150        if g.params.individual:
151            work_df = count_df.query("type == 2").filter(items=["matter", "matter_count", "ex_total"])
152        else:
153            work_df = count_df.query("type == 2 or type == 3").filter(items=["matter", "matter_count", "ex_total"])
154        m.set_message(work_df, StyleOptions(title="卓外清算", data_kind=StyleOptions.DataKind.REMARKS_REGULATION))
155
156    if not g.cfg.rule.dropitems(g.params.rule_version) & g.cfg.dropitems.other:
157        work_df = count_df.query("type == 1").filter(items=["matter", "matter_count"])
158        m.set_message(work_df, StyleOptions(title="その他", data_kind=StyleOptions.DataKind.REMARKS_OTHER))
159
160    # 対戦結果
161    if g.params.versus_matrix:
162        m.set_message(get_versus_matrix(mapping_dict), StyleOptions(title="対戦結果", indent=1))
163
164    # 戦績
165    if g.params.game_results:
166        if g.params.verbose:
167            m.set_message(get_results_details(mapping_dict), StyleOptions(title="戦績", data_kind=StyleOptions.DataKind.RECORD_DATA_ALL, codeblock=False))
168        else:
169            m.set_message(get_results_simple(mapping_dict), StyleOptions(title="戦績", data_kind=StyleOptions.DataKind.RECORD_DATA, codeblock=False))
170
171    # 非表示項目を除外
172    m.post.message = [(data, options) for data, options in m.post.message if options.title not in g.cfg.rule.dropitems(g.params.rule_version)]
173
174    m.set_headline(message_build(msg_data), StyleOptions(title=title))
175
176
177def comparison(m: "MessageParserProtocol") -> None:
178    """
179    成績詳細を比較
180
181    Args:
182        m (MessageParserProtocol): メッセージデータ
183
184    """
185    # 検索動作を合わせる
186    g.params.guest_skip = g.params.guest_skip2
187
188    if g.params.player_name in g.cfg.team.lists:
189        g.params.update_from_dict({"individual": False})
190    elif g.params.player_name in g.cfg.member.lists:
191        g.params.update_from_dict({"individual": True})
192
193    # データ収集
194    data: "MessageType"
195    game_info = GameInfo()
196
197    # タイトル
198    title = "成績詳細比較"
199    m.set_headline(message.header(game_info, m, "", 1), StyleOptions(title=title))
200
201    if not game_info.count:
202        m.status.result = False
203        return
204
205    stats_df = pd.DataFrame()
206    result_df = g.params.read_data("RESULTS_INFO")
207    record_df = g.params.read_data("RECORD_INFO")
208
209    for name in result_df.query("id==0").sort_values("total_point", ascending=False)["name"]:
210        work_stats = StatsInfo()
211        if str(name) not in g.params.player_list:
212            continue
213        work_stats.set_parameter(**g.params.placeholder())
214        work_stats.name = str(name)
215        work_stats.set_data(result_df.query("name == @name"))
216        work_stats.set_data(record_df.query("name == @name"))
217        stats_df = pd.concat([stats_df, work_stats.summary])
218
219    if stats_df.empty:
220        m.set_headline(message.random_reply(m, "no_hits"), StyleOptions())
221        m.status.result = False
222        return
223
224    # 規定打数足切り
225    stats_df.query("count >= @g.params.stipulated", inplace=True)
226    if stats_df.empty:
227        m.set_headline(message.random_reply(m, "no_target"), StyleOptions())
228        m.status.result = False
229        return
230
231    if g.params.anonymous:
232        mapping_dict = formatter.anonymous_mapping(stats_df["name"].unique().tolist())
233        stats_df["name"] = stats_df["name"].replace(mapping_dict)
234
235    # 非表示項目
236    stats_df = stats_df.drop(columns=[x for x in g.cfg.rule.dropitems(g.params.rule_version) if x in stats_df.columns.to_list()])
237    if g.params.ignore_flying or g.cfg.rule.dropitems(g.params.rule_version) & g.cfg.dropitems.flying:
238        stats_df = stats_df.drop(columns=["flying_rate-count"])
239    if g.cfg.rule.dropitems(g.params.rule_version) & g.cfg.dropitems.yakuman:
240        stats_df = stats_df.drop(columns=["yakuman_rate-count"])
241
242    # 出力
243    options: StyleOptions = StyleOptions(
244        title=title,
245        data_kind=StyleOptions.DataKind.DETAILED_COMPARISON,
246        base_name=title,
247        show_index=True,
248        codeblock=True,
249        transpose=True,
250    )
251
252    match g.params.format.lower():
253        case "csv":
254            options.format_type = "csv"
255            data = converter.save_output(stats_df, options, m.post.headline)
256        case "text" | "txt":
257            options.format_type = "txt"
258            data = converter.save_output(stats_df, options, m.post.headline)
259        case _:
260            options.key_title = False
261            data = formatter.df_rename(stats_df, options).T
262
263    m.set_message(data, options)
264    m.post.thread = True
265
266
267def get_headline(data: StatsInfo, game_info: GameInfo, player_name: str) -> dict[str, Any]:
268    """
269    ヘッダメッセージ生成
270
271    Args:
272        data (dict): 生成内容が格納された辞書
273        game_info (GameInfo): ゲーム集計情報
274        player_name (str): プレイヤー名
275
276    Returns:
277        dict[str, Any]: 集計データ
278
279    """
280    ret: dict[str, Any] = {}
281
282    if g.params.individual:
283        ret["プレイヤー名"] = f"{player_name} {badge.degree(data.seat0.count)}"
284        if team_name := g.cfg.team.which(g.params.player_name):
285            ret["所属チーム"] = team_name
286    else:
287        ret["チーム名"] = f"{g.params.player_name} {badge.degree(data.seat0.count)}"
288        ret["登録メンバー"] = "、".join(g.cfg.team.member(g.params.player_name))
289
290    badge_status = badge.status(data.seat0.count, data.seat0.win)
291    ret["検索範囲"] = str(text_item.search_range(time_pattern="time"))
292    ret["集計範囲"] = str(text_item.aggregation_range(game_info))
293    ret["特記事項"] = "、".join(text_item.remarks())
294    ret["検索ワード"] = text_item.search_word()
295    ret["対戦数"] = f"{data.seat0.war_record()} {badge_status}"
296    ret["_blank1"] = True
297
298    return ret
299
300
301def get_totalization(data: StatsInfo) -> dict[str, Any]:
302    """
303    集計トータルメッセージ生成
304
305    Args:
306        data (StatsInfo): 成績情報
307
308    Returns:
309        dict[str, Any]: 生成メッセージ
310
311    """
312    ret: dict[str, Any] = {}
313
314    ret["通算ポイント"] = f"{data.seat0.total_point:+.1f}pt".replace("-", "▲")
315    ret["平均ポイント"] = f"{data.seat0.avg_point:+.1f}pt".replace("-", "▲")
316    ret["平均順位"] = f"{data.seat0.rank_avg:1.2f}"
317    if g.params.individual and g.adapter.conf.badge_grade:
318        ret["段位"] = badge.grade(g.params.player_name)
319    ret["_blank2"] = True
320    ret["1位"] = f"{data.seat0.rank1:2} 回 ({data.seat0.rank1_rate:7.2%})"
321    ret["2位"] = f"{data.seat0.rank2:2} 回 ({data.seat0.rank2_rate:7.2%})"
322    ret["3位"] = f"{data.seat0.rank3:2} 回 ({data.seat0.rank3_rate:7.2%})"
323    if g.params.mode == 4:
324        ret["4位"] = f"{data.seat0.rank4:2} 回 ({data.seat0.rank4_rate:7.2%})"
325    ret["トビ"] = f"{data.seat0.flying:2} 回 ({data.seat0.flying_rate:7.2%})"
326    ret["役満"] = f"{data.seat0.yakuman:2} 回 ({data.seat0.yakuman_rate:7.2%})"
327
328    return ret
329
330
331def get_results_simple(mapping_dict: dict[str, str]) -> pd.DataFrame:
332    """
333    戦績(簡易)データ取得
334
335    Args:
336        mapping_dict (dict[str, str]): 匿名化オプション用マップ
337
338    Returns:
339        pd.DataFrame: 戦績データ
340
341    """
342    target_player = formatter.name_replace(g.params.target_player[0], add_mark=True)
343
344    df = g.params.read_data("SUMMARY_DETAILS").fillna(value="")
345    if g.params.anonymous:
346        mapping_dict.update(formatter.anonymous_mapping(df["name"].unique().tolist(), len(mapping_dict)))
347        df["name"] = df["name"].replace(mapping_dict)
348        target_player = mapping_dict.get(target_player, target_player)
349
350    df_data = df.query("name == @target_player")
351    df_data["seat"] = df_data.apply(lambda v: ["東家", "南家", "西家", "北家"][(v["seat"] - 1)], axis=1)
352    df_data["rpoint"] = df_data["rpoint"] * 100
353    pd.options.mode.copy_on_write = True
354    if g.params.individual:
355        df_data.loc[:, "memo"] = np.where(df_data["guest_count"] >= 2, "2ゲスト戦", "")
356    else:
357        df_data.loc[:, "memo"] = np.where(df_data["same_team"] == 1, "チーム同卓", "")
358    df_data = df_data.filter(items=["playtime", "seat", "rank", "rpoint", "point", "remarks", "memo"])
359
360    return df_data
361
362
363def get_results_details(mapping_dict: dict[str, str]) -> pd.DataFrame:
364    """
365    戦績(詳細)データ取得
366
367    Args:
368        mapping_dict (dict[str, str]): 匿名化オプション用マップ
369
370    Returns:
371        pd.DataFrame: 戦績データ
372
373    """
374    target_player = formatter.name_replace(g.params.target_player[0], add_mark=True)  # noqa: F841
375
376    df = g.params.read_data("SUMMARY_DETAILS2").fillna(value="")
377    if g.params.anonymous:
378        name_list: list[str] = []
379        name_list.extend(df["p1_name"].unique().tolist())
380        name_list.extend(df["p2_name"].unique().tolist())
381        name_list.extend(df["p3_name"].unique().tolist())
382        name_list.extend(df["p4_name"].unique().tolist())
383        mapping_dict.update(formatter.anonymous_mapping(list(set(name_list)), len(mapping_dict)))
384        df["p1_name"] = df["p1_name"].replace(mapping_dict)
385        df["p2_name"] = df["p2_name"].replace(mapping_dict)
386        df["p3_name"] = df["p3_name"].replace(mapping_dict)
387        df["p4_name"] = df["p4_name"].replace(mapping_dict)
388        target_player = mapping_dict.get(target_player, target_player)
389
390    match g.params.mode:
391        case 3:
392            df.drop(columns=["p4_name", "p4_rpoint", "p4_rank", "p4_point", "p4_remarks"], inplace=True)
393            df_data = df.query(
394                "p1_name == @target_player or p2_name == @target_player or p3_name == @target_player"  # noqa: E501
395            )
396        case 4:
397            df_data = df.query(
398                "p1_name == @target_player or p2_name == @target_player or p3_name == @target_player or p4_name == @target_player"  # noqa: E501
399            )
400        case _:
401            return pd.DataFrame()
402
403    pd.options.mode.copy_on_write = True
404    if g.params.individual:
405        df_data.loc[:, "memo"] = np.where(df_data["guest_count"] >= 2, "2ゲスト戦", "")
406    else:
407        df_data.loc[:, "memo"] = np.where(df_data["same_team"] == 1, "チーム同卓", "")
408    df_data = df_data.drop(columns=["guest_count", "same_team"])
409
410    return df_data
411
412
413def get_versus_matrix(mapping_dict: dict[str, str]) -> str:
414    """
415    対戦結果データ出力用メッセージ生成
416
417    Args:
418        mapping_dict (dict[str, str]): 匿名化用マッピングデータ
419
420    Returns:
421        str: 出力メッセージ
422
423    """
424    df = g.params.read_data("SUMMARY_VERSUS_MATRIX")
425
426    if df.empty:
427        return ""
428
429    if g.params.anonymous:
430        mapping_dict.update(formatter.anonymous_mapping(df["vs_name"].unique().tolist(), len(mapping_dict)))
431        df["my_name"] = df["my_name"].replace(mapping_dict)
432        df["vs_name"] = df["vs_name"].replace(mapping_dict)
433
434    data_list: list[list[str]] = []
435    for _, r in df.iterrows():
436        data_list.append([r["vs_name"], f"{r['game']} 戦", f"{r['win']} 勝", f"{r['lose']} 敗", f"({r['win%']:6.2f}%)"])
437
438    output = table2ascii(
439        # header=["対戦相手", "ゲーム数", "勝", "負", "勝率"],
440        body=data_list,
441        alignments=[Alignment.LEFT, Alignment.RIGHT, Alignment.RIGHT, Alignment.RIGHT, Alignment.RIGHT],
442        style=PresetStyle.ascii_borderless,
443        cell_padding=0,
444    )
445
446    return output
447
448
449def message_build(data: dict[str, str]) -> str:
450    """
451    表示する内容をテキストに起こす
452
453    Args:
454        data (dict[str, str]): 内容
455
456    Returns:
457        str: 表示するテキスト
458
459    """
460    msg = ""
461    for k, v in data.items():
462        if not v:  # 値がない項目は削除
463            continue
464        match k:
465            case k if k in g.cfg.rule.dropitems(g.params.rule_version):  # 非表示
466                pass
467            case k if str(k).startswith("_blank"):
468                msg += "\n"
469            case "title":
470                msg += f"{v}\n"
471            case _:
472                msg += f"{k}{v}\n"
473
474    return textwrap.indent(msg.strip(), "\t")
def aggregation(m: integrations.protocols.MessageParserProtocol) -> None:
 26def aggregation(m: "MessageParserProtocol") -> None:
 27    """
 28    成績詳細を集計
 29
 30    Args:
 31        m (MessageParserProtocol): メッセージデータ
 32
 33    """
 34    # --- パラメータ更新
 35    g.params.guest_skip = g.params.guest_skip2  # 検索動作を合わせる
 36
 37    if rule_version := g.params.rule_version:
 38        g.params.update_from_dict(
 39            {
 40                "mode": int(g.cfg.rule.to_dict(rule_version).get("mode", 4)),
 41                "rule_version": str(g.cfg.rule.to_dict(rule_version).get("rule_version", "")),
 42                "origin_point": int(g.cfg.rule.to_dict(rule_version).get("origin_point", 250)),
 43                "return_point": int(g.cfg.rule.to_dict(rule_version).get("return_point", 300)),
 44            }
 45        )
 46        if (target_mode := g.params.target_mode) and target_mode != g.cfg.rule.get_mode(rule_version):
 47            m.set_headline(message.random_reply(m, "rule_mismatch"), StyleOptions(title="集計矛盾検出"))
 48            m.status.result = False
 49            return
 50    if g.params.player_name in g.cfg.team.lists:
 51        g.params.individual = False
 52    elif g.params.player_name in g.cfg.member.lists:
 53        g.params.individual = True
 54
 55    # --- データ収集
 56    game_info = GameInfo()
 57    msg_data: dict[str, str] = {}
 58    mapping_dict: dict[str, str] = {}
 59
 60    # タイトル
 61    if g.params.individual:
 62        title = "個人成績詳細"
 63    else:
 64        title = "チーム成績詳細"
 65
 66    if game_info.count == 0:
 67        if g.params.individual:
 68            msg_data["検索範囲"] = f"{text_item.search_range(time_pattern='time')}"
 69            msg_data["特記事項"] = "、".join(text_item.remarks())
 70            msg_data["検索ワード"] = text_item.search_word()
 71            msg_data["対戦数"] = f"0 戦 (0 勝 0 敗 0 分) {badge.status(0, 0)}"
 72            m.set_headline(message_build(msg_data), StyleOptions(title=title))
 73        else:
 74            m.set_headline("登録されていないチームです。", StyleOptions(title=title))
 75        m.status.result = False
 76        return
 77
 78    stats = StatsInfo()
 79    stats.read(g.params)
 80
 81    if stats.result_df.empty or stats.record_df.empty:
 82        m.set_headline(message.random_reply(m, "no_target"), StyleOptions(title=title))
 83        m.status.result = False
 84        return
 85
 86    player_name = formatter.name_replace(g.params.player_name, add_mark=True)
 87    if g.params.anonymous:
 88        mapping_dict = formatter.anonymous_mapping(stats.result_df["name"].unique().tolist())
 89        stats.result_df["name"] = stats.result_df["name"].replace(mapping_dict)
 90        player_name = mapping_dict[player_name]
 91
 92    # --- 表示内容
 93    msg_data.update(get_headline(stats, game_info, player_name))
 94    msg_data.update(get_totalization(stats))
 95    mode = g.params.mode
 96
 97    # 統計
 98    seat_data = pd.DataFrame(
 99        {  # 座席データ
100            "席": ["東家", "南家", "西家", "北家"][:mode],
101            "順位分布": stats.rank_distr_list2,
102            "平均順位": [f"{x:.2f}".replace("0.00", "-.--") for x in stats.rank_avg_list],
103            "トビ": stats.flying_list,
104            "役満和了": stats.yakuman_list,
105        }
106    )
107
108    if g.params.ignore_flying or g.cfg.rule.dropitems(g.params.rule_version) & g.cfg.dropitems.flying:
109        seat_data.drop(columns=["トビ"], inplace=True)
110    if g.cfg.rule.dropitems(g.params.rule_version) & g.cfg.dropitems.yakuman:
111        seat_data.drop(columns=["役満和了"], inplace=True)
112
113    if mode == 3:
114        balance_data = textwrap.dedent(
115            f"""\
116            全体:{stats.seat0.avg_balance("all"):+.1f}
117            1着終了時:{stats.seat0.avg_balance("rank1"):+.1f}
118            2着終了時:{stats.seat0.avg_balance("rank2"):+.1f}
119            3着終了時:{stats.seat0.avg_balance("rank3"):+.1f}
120            """.replace("+0.0点", "記録なし")
121        ).replace("-", "▲")
122    else:
123        balance_data = textwrap.dedent(
124            f"""\
125            全体:{stats.seat0.avg_balance("all"):+.1f}
126            連対時:{stats.seat0.avg_balance("top2"):+.1f}
127            逆連対時:{stats.seat0.avg_balance("lose2"):+.1f}
128            1着終了時:{stats.seat0.avg_balance("rank1"):+.1f}
129            2着終了時:{stats.seat0.avg_balance("rank2"):+.1f}
130            3着終了時:{stats.seat0.avg_balance("rank3"):+.1f}
131            4着終了時:{stats.seat0.avg_balance("rank4"):+.1f}
132            """.replace("+0.0点", "記録なし")
133        ).replace("-", "▲")
134
135    if g.params.statistics:
136        m.set_message(seat_data, StyleOptions(title="座席データ", data_kind=StyleOptions.DataKind.SEAT_DATA))
137        m.set_message(textwrap.indent(stats.seat0.best_record(), "\t"), StyleOptions(title="ベストレコード"))
138        m.set_message(textwrap.indent(stats.seat0.worst_record(), "\t"), StyleOptions(title="ワーストレコード"))
139        m.set_message(textwrap.indent(balance_data.strip(), "\t"), StyleOptions(title="平均収支"))
140
141    # レギュレーション
142    remarks_df = g.params.read_data("REMARKS_INFO")
143    count_df = remarks_df.groupby("matter").agg(matter_count=("matter", "count"), ex_total=("ex_point", "sum"), type=("type", "max"))
144    count_df["matter"] = count_df.index
145
146    if not g.cfg.rule.dropitems(g.params.rule_version) & g.cfg.dropitems.yakuman:
147        work_df = count_df.query("type == 0").filter(items=["matter", "matter_count"])
148        m.set_message(work_df, StyleOptions(title="役満和了", data_kind=StyleOptions.DataKind.REMARKS_YAKUMAN))
149
150    if not g.cfg.rule.dropitems(g.params.rule_version) & g.cfg.dropitems.regulation:
151        if g.params.individual:
152            work_df = count_df.query("type == 2").filter(items=["matter", "matter_count", "ex_total"])
153        else:
154            work_df = count_df.query("type == 2 or type == 3").filter(items=["matter", "matter_count", "ex_total"])
155        m.set_message(work_df, StyleOptions(title="卓外清算", data_kind=StyleOptions.DataKind.REMARKS_REGULATION))
156
157    if not g.cfg.rule.dropitems(g.params.rule_version) & g.cfg.dropitems.other:
158        work_df = count_df.query("type == 1").filter(items=["matter", "matter_count"])
159        m.set_message(work_df, StyleOptions(title="その他", data_kind=StyleOptions.DataKind.REMARKS_OTHER))
160
161    # 対戦結果
162    if g.params.versus_matrix:
163        m.set_message(get_versus_matrix(mapping_dict), StyleOptions(title="対戦結果", indent=1))
164
165    # 戦績
166    if g.params.game_results:
167        if g.params.verbose:
168            m.set_message(get_results_details(mapping_dict), StyleOptions(title="戦績", data_kind=StyleOptions.DataKind.RECORD_DATA_ALL, codeblock=False))
169        else:
170            m.set_message(get_results_simple(mapping_dict), StyleOptions(title="戦績", data_kind=StyleOptions.DataKind.RECORD_DATA, codeblock=False))
171
172    # 非表示項目を除外
173    m.post.message = [(data, options) for data, options in m.post.message if options.title not in g.cfg.rule.dropitems(g.params.rule_version)]
174
175    m.set_headline(message_build(msg_data), StyleOptions(title=title))

成績詳細を集計

Arguments:
  • m (MessageParserProtocol): メッセージデータ
def comparison(m: integrations.protocols.MessageParserProtocol) -> None:
178def comparison(m: "MessageParserProtocol") -> None:
179    """
180    成績詳細を比較
181
182    Args:
183        m (MessageParserProtocol): メッセージデータ
184
185    """
186    # 検索動作を合わせる
187    g.params.guest_skip = g.params.guest_skip2
188
189    if g.params.player_name in g.cfg.team.lists:
190        g.params.update_from_dict({"individual": False})
191    elif g.params.player_name in g.cfg.member.lists:
192        g.params.update_from_dict({"individual": True})
193
194    # データ収集
195    data: "MessageType"
196    game_info = GameInfo()
197
198    # タイトル
199    title = "成績詳細比較"
200    m.set_headline(message.header(game_info, m, "", 1), StyleOptions(title=title))
201
202    if not game_info.count:
203        m.status.result = False
204        return
205
206    stats_df = pd.DataFrame()
207    result_df = g.params.read_data("RESULTS_INFO")
208    record_df = g.params.read_data("RECORD_INFO")
209
210    for name in result_df.query("id==0").sort_values("total_point", ascending=False)["name"]:
211        work_stats = StatsInfo()
212        if str(name) not in g.params.player_list:
213            continue
214        work_stats.set_parameter(**g.params.placeholder())
215        work_stats.name = str(name)
216        work_stats.set_data(result_df.query("name == @name"))
217        work_stats.set_data(record_df.query("name == @name"))
218        stats_df = pd.concat([stats_df, work_stats.summary])
219
220    if stats_df.empty:
221        m.set_headline(message.random_reply(m, "no_hits"), StyleOptions())
222        m.status.result = False
223        return
224
225    # 規定打数足切り
226    stats_df.query("count >= @g.params.stipulated", inplace=True)
227    if stats_df.empty:
228        m.set_headline(message.random_reply(m, "no_target"), StyleOptions())
229        m.status.result = False
230        return
231
232    if g.params.anonymous:
233        mapping_dict = formatter.anonymous_mapping(stats_df["name"].unique().tolist())
234        stats_df["name"] = stats_df["name"].replace(mapping_dict)
235
236    # 非表示項目
237    stats_df = stats_df.drop(columns=[x for x in g.cfg.rule.dropitems(g.params.rule_version) if x in stats_df.columns.to_list()])
238    if g.params.ignore_flying or g.cfg.rule.dropitems(g.params.rule_version) & g.cfg.dropitems.flying:
239        stats_df = stats_df.drop(columns=["flying_rate-count"])
240    if g.cfg.rule.dropitems(g.params.rule_version) & g.cfg.dropitems.yakuman:
241        stats_df = stats_df.drop(columns=["yakuman_rate-count"])
242
243    # 出力
244    options: StyleOptions = StyleOptions(
245        title=title,
246        data_kind=StyleOptions.DataKind.DETAILED_COMPARISON,
247        base_name=title,
248        show_index=True,
249        codeblock=True,
250        transpose=True,
251    )
252
253    match g.params.format.lower():
254        case "csv":
255            options.format_type = "csv"
256            data = converter.save_output(stats_df, options, m.post.headline)
257        case "text" | "txt":
258            options.format_type = "txt"
259            data = converter.save_output(stats_df, options, m.post.headline)
260        case _:
261            options.key_title = False
262            data = formatter.df_rename(stats_df, options).T
263
264    m.set_message(data, options)
265    m.post.thread = True

成績詳細を比較

Arguments:
  • m (MessageParserProtocol): メッセージデータ
def get_headline( data: libs.domain.stats.StatsInfo, game_info: libs.domain.datamodels.GameInfo, player_name: str) -> dict[str, typing.Any]:
268def get_headline(data: StatsInfo, game_info: GameInfo, player_name: str) -> dict[str, Any]:
269    """
270    ヘッダメッセージ生成
271
272    Args:
273        data (dict): 生成内容が格納された辞書
274        game_info (GameInfo): ゲーム集計情報
275        player_name (str): プレイヤー名
276
277    Returns:
278        dict[str, Any]: 集計データ
279
280    """
281    ret: dict[str, Any] = {}
282
283    if g.params.individual:
284        ret["プレイヤー名"] = f"{player_name} {badge.degree(data.seat0.count)}"
285        if team_name := g.cfg.team.which(g.params.player_name):
286            ret["所属チーム"] = team_name
287    else:
288        ret["チーム名"] = f"{g.params.player_name} {badge.degree(data.seat0.count)}"
289        ret["登録メンバー"] = "、".join(g.cfg.team.member(g.params.player_name))
290
291    badge_status = badge.status(data.seat0.count, data.seat0.win)
292    ret["検索範囲"] = str(text_item.search_range(time_pattern="time"))
293    ret["集計範囲"] = str(text_item.aggregation_range(game_info))
294    ret["特記事項"] = "、".join(text_item.remarks())
295    ret["検索ワード"] = text_item.search_word()
296    ret["対戦数"] = f"{data.seat0.war_record()} {badge_status}"
297    ret["_blank1"] = True
298
299    return ret

ヘッダメッセージ生成

Arguments:
  • data (dict): 生成内容が格納された辞書
  • game_info (GameInfo): ゲーム集計情報
  • player_name (str): プレイヤー名
Returns:

dict[str, Any]: 集計データ

def get_totalization(data: libs.domain.stats.StatsInfo) -> dict[str, typing.Any]:
302def get_totalization(data: StatsInfo) -> dict[str, Any]:
303    """
304    集計トータルメッセージ生成
305
306    Args:
307        data (StatsInfo): 成績情報
308
309    Returns:
310        dict[str, Any]: 生成メッセージ
311
312    """
313    ret: dict[str, Any] = {}
314
315    ret["通算ポイント"] = f"{data.seat0.total_point:+.1f}pt".replace("-", "▲")
316    ret["平均ポイント"] = f"{data.seat0.avg_point:+.1f}pt".replace("-", "▲")
317    ret["平均順位"] = f"{data.seat0.rank_avg:1.2f}"
318    if g.params.individual and g.adapter.conf.badge_grade:
319        ret["段位"] = badge.grade(g.params.player_name)
320    ret["_blank2"] = True
321    ret["1位"] = f"{data.seat0.rank1:2} 回 ({data.seat0.rank1_rate:7.2%})"
322    ret["2位"] = f"{data.seat0.rank2:2} 回 ({data.seat0.rank2_rate:7.2%})"
323    ret["3位"] = f"{data.seat0.rank3:2} 回 ({data.seat0.rank3_rate:7.2%})"
324    if g.params.mode == 4:
325        ret["4位"] = f"{data.seat0.rank4:2} 回 ({data.seat0.rank4_rate:7.2%})"
326    ret["トビ"] = f"{data.seat0.flying:2} 回 ({data.seat0.flying_rate:7.2%})"
327    ret["役満"] = f"{data.seat0.yakuman:2} 回 ({data.seat0.yakuman_rate:7.2%})"
328
329    return ret

集計トータルメッセージ生成

Arguments:
  • data (StatsInfo): 成績情報
Returns:

dict[str, Any]: 生成メッセージ

def get_results_simple(mapping_dict: dict[str, str]) -> pandas.DataFrame:
332def get_results_simple(mapping_dict: dict[str, str]) -> pd.DataFrame:
333    """
334    戦績(簡易)データ取得
335
336    Args:
337        mapping_dict (dict[str, str]): 匿名化オプション用マップ
338
339    Returns:
340        pd.DataFrame: 戦績データ
341
342    """
343    target_player = formatter.name_replace(g.params.target_player[0], add_mark=True)
344
345    df = g.params.read_data("SUMMARY_DETAILS").fillna(value="")
346    if g.params.anonymous:
347        mapping_dict.update(formatter.anonymous_mapping(df["name"].unique().tolist(), len(mapping_dict)))
348        df["name"] = df["name"].replace(mapping_dict)
349        target_player = mapping_dict.get(target_player, target_player)
350
351    df_data = df.query("name == @target_player")
352    df_data["seat"] = df_data.apply(lambda v: ["東家", "南家", "西家", "北家"][(v["seat"] - 1)], axis=1)
353    df_data["rpoint"] = df_data["rpoint"] * 100
354    pd.options.mode.copy_on_write = True
355    if g.params.individual:
356        df_data.loc[:, "memo"] = np.where(df_data["guest_count"] >= 2, "2ゲスト戦", "")
357    else:
358        df_data.loc[:, "memo"] = np.where(df_data["same_team"] == 1, "チーム同卓", "")
359    df_data = df_data.filter(items=["playtime", "seat", "rank", "rpoint", "point", "remarks", "memo"])
360
361    return df_data

戦績(簡易)データ取得

Arguments:
  • mapping_dict (dict[str, str]): 匿名化オプション用マップ
Returns:

pd.DataFrame: 戦績データ

def get_results_details(mapping_dict: dict[str, str]) -> pandas.DataFrame:
364def get_results_details(mapping_dict: dict[str, str]) -> pd.DataFrame:
365    """
366    戦績(詳細)データ取得
367
368    Args:
369        mapping_dict (dict[str, str]): 匿名化オプション用マップ
370
371    Returns:
372        pd.DataFrame: 戦績データ
373
374    """
375    target_player = formatter.name_replace(g.params.target_player[0], add_mark=True)  # noqa: F841
376
377    df = g.params.read_data("SUMMARY_DETAILS2").fillna(value="")
378    if g.params.anonymous:
379        name_list: list[str] = []
380        name_list.extend(df["p1_name"].unique().tolist())
381        name_list.extend(df["p2_name"].unique().tolist())
382        name_list.extend(df["p3_name"].unique().tolist())
383        name_list.extend(df["p4_name"].unique().tolist())
384        mapping_dict.update(formatter.anonymous_mapping(list(set(name_list)), len(mapping_dict)))
385        df["p1_name"] = df["p1_name"].replace(mapping_dict)
386        df["p2_name"] = df["p2_name"].replace(mapping_dict)
387        df["p3_name"] = df["p3_name"].replace(mapping_dict)
388        df["p4_name"] = df["p4_name"].replace(mapping_dict)
389        target_player = mapping_dict.get(target_player, target_player)
390
391    match g.params.mode:
392        case 3:
393            df.drop(columns=["p4_name", "p4_rpoint", "p4_rank", "p4_point", "p4_remarks"], inplace=True)
394            df_data = df.query(
395                "p1_name == @target_player or p2_name == @target_player or p3_name == @target_player"  # noqa: E501
396            )
397        case 4:
398            df_data = df.query(
399                "p1_name == @target_player or p2_name == @target_player or p3_name == @target_player or p4_name == @target_player"  # noqa: E501
400            )
401        case _:
402            return pd.DataFrame()
403
404    pd.options.mode.copy_on_write = True
405    if g.params.individual:
406        df_data.loc[:, "memo"] = np.where(df_data["guest_count"] >= 2, "2ゲスト戦", "")
407    else:
408        df_data.loc[:, "memo"] = np.where(df_data["same_team"] == 1, "チーム同卓", "")
409    df_data = df_data.drop(columns=["guest_count", "same_team"])
410
411    return df_data

戦績(詳細)データ取得

Arguments:
  • mapping_dict (dict[str, str]): 匿名化オプション用マップ
Returns:

pd.DataFrame: 戦績データ

def get_versus_matrix(mapping_dict: dict[str, str]) -> str:
414def get_versus_matrix(mapping_dict: dict[str, str]) -> str:
415    """
416    対戦結果データ出力用メッセージ生成
417
418    Args:
419        mapping_dict (dict[str, str]): 匿名化用マッピングデータ
420
421    Returns:
422        str: 出力メッセージ
423
424    """
425    df = g.params.read_data("SUMMARY_VERSUS_MATRIX")
426
427    if df.empty:
428        return ""
429
430    if g.params.anonymous:
431        mapping_dict.update(formatter.anonymous_mapping(df["vs_name"].unique().tolist(), len(mapping_dict)))
432        df["my_name"] = df["my_name"].replace(mapping_dict)
433        df["vs_name"] = df["vs_name"].replace(mapping_dict)
434
435    data_list: list[list[str]] = []
436    for _, r in df.iterrows():
437        data_list.append([r["vs_name"], f"{r['game']} 戦", f"{r['win']} 勝", f"{r['lose']} 敗", f"({r['win%']:6.2f}%)"])
438
439    output = table2ascii(
440        # header=["対戦相手", "ゲーム数", "勝", "負", "勝率"],
441        body=data_list,
442        alignments=[Alignment.LEFT, Alignment.RIGHT, Alignment.RIGHT, Alignment.RIGHT, Alignment.RIGHT],
443        style=PresetStyle.ascii_borderless,
444        cell_padding=0,
445    )
446
447    return output

対戦結果データ出力用メッセージ生成

Arguments:
  • mapping_dict (dict[str, str]): 匿名化用マッピングデータ
Returns:

str: 出力メッセージ

def message_build(data: dict[str, str]) -> str:
450def message_build(data: dict[str, str]) -> str:
451    """
452    表示する内容をテキストに起こす
453
454    Args:
455        data (dict[str, str]): 内容
456
457    Returns:
458        str: 表示するテキスト
459
460    """
461    msg = ""
462    for k, v in data.items():
463        if not v:  # 値がない項目は削除
464            continue
465        match k:
466            case k if k in g.cfg.rule.dropitems(g.params.rule_version):  # 非表示
467                pass
468            case k if str(k).startswith("_blank"):
469                msg += "\n"
470            case "title":
471                msg += f"{v}\n"
472            case _:
473                msg += f"{k}{v}\n"
474
475    return textwrap.indent(msg.strip(), "\t")

表示する内容をテキストに起こす

Arguments:
  • data (dict[str, str]): 内容
Returns:

str: 表示するテキスト