libs.functions.tools.score_simulator

libs/functions/tools/score_simulator.py

得点シミュレーター

Returns:

list: ゲーム終了時点の素点リスト

  1"""
  2libs/functions/tools/score_simulator.py
  3
  4得点シミュレーター
  5
  6Returns:
  7    list: ゲーム終了時点の素点リスト
  8
  9"""
 10
 11import random
 12from typing import Union
 13
 14INITIAL_POINTS: int = 25000
 15MAX_ROUNDS: int = 8
 16
 17# 点数テーブル
 18HAN_POINTS: dict[int, dict[str, Union[int, tuple[int, ...]]]] = {
 19    1: {"ron_child": 1300, "ron_parent": 2000, "tsumo_child": (300, 500), "tsumo_parent": (500,)},
 20    2: {"ron_child": 2600, "ron_parent": 3900, "tsumo_child": (500, 1000), "tsumo_parent": (1000,)},
 21    3: {"ron_child": 5200, "ron_parent": 7700, "tsumo_child": (1000, 2000), "tsumo_parent": (2000,)},
 22    4: {"ron_child": 8000, "ron_parent": 12000, "tsumo_child": (2000, 4000), "tsumo_parent": (4000,)},
 23    5: {"ron_child": 8000, "ron_parent": 12000, "tsumo_child": (2000, 4000), "tsumo_parent": (4000,)},
 24    6: {"ron_child": 12000, "ron_parent": 18000, "tsumo_child": (3000, 6000), "tsumo_parent": (6000,)},
 25    7: {"ron_child": 12000, "ron_parent": 18000, "tsumo_child": (3000, 6000), "tsumo_parent": (6000,)},
 26    8: {"ron_child": 16000, "ron_parent": 24000, "tsumo_child": (4000, 8000), "tsumo_parent": (8000,)},
 27    9: {"ron_child": 16000, "ron_parent": 24000, "tsumo_child": (4000, 8000), "tsumo_parent": (8000,)},
 28    10: {"ron_child": 24000, "ron_parent": 36000, "tsumo_child": (6000, 12000), "tsumo_parent": (12000,)},
 29    11: {"ron_child": 24000, "ron_parent": 36000, "tsumo_child": (6000, 12000), "tsumo_parent": (12000,)},
 30    12: {"ron_child": 24000, "ron_parent": 36000, "tsumo_child": (6000, 12000), "tsumo_parent": (12000,)},
 31    13: {"ron_child": 32000, "ron_parent": 48000, "tsumo_child": (8000, 16000), "tsumo_parent": (16000,)},
 32    14: {"ron_child": 32000, "ron_parent": 48000, "tsumo_child": (8000, 16000), "tsumo_parent": (16000,)},
 33    15: {"ron_child": 64000, "ron_parent": 96000, "tsumo_child": (16000, 32000), "tsumo_parent": (32000,)},  # ダブル役満扱い
 34}
 35
 36
 37def determine_point(is_parent: bool, is_tsumo: bool) -> int | tuple[int, ...]:
 38    """
 39    和了打点を決める
 40
 41    Args:
 42        is_parent (bool): 親フラグ
 43        is_tsumo (bool): ツモ/被ツモフラグ
 44
 45    Returns:
 46        int | tuple: 打点
 47
 48    """
 49    rank = 1
 50    while rank < 15:
 51        success_prob = max([0, 0.6 - 0.02 * rank])
 52        if random.random() > success_prob:
 53            break
 54        rank += 1
 55
 56    key = "tsumo_parent" if is_parent and is_tsumo else "tsumo_child" if not is_parent and is_tsumo else "ron_parent" if is_parent else "ron_child"
 57
 58    return HAN_POINTS[rank][key]
 59
 60
 61def determine_winner(k: int) -> tuple[list[int], list[int]]:
 62    """
 63    和了役を抽選し、放銃役候補と分けてリストを返す
 64
 65    Args:
 66        k (int): 和了役に選ばれる人数
 67
 68    Returns:
 69        tuple[list[int], list[int]]: 抽選結果
 70
 71    """
 72    member: list[int] = list(range(4))
 73    winners: list[int] = random.sample(member, k=k)  # 和了役
 74    losers: list[int] = [i for i in member if i not in winners]
 75
 76    return (winners, losers)
 77
 78
 79def should_renchan(winners: list[int], parent: int, tenpai: list[bool], total_rounds: int, renchan_count: int) -> tuple[int, int, int]:
 80    """
 81    連チャンの判定を行う
 82
 83    Args:
 84        winners (list[int]): 和了者のリスト(流局時は空リスト)
 85        parent (int): 現在の親
 86        tenpai (list[bool]): 流局時のテンパイ状況(和了時は空リスト)
 87        total_rounds (int): 現在の局数
 88        renchan_count (int): 現在の連チャン数
 89
 90
 91    Returns:
 92        tuple[int, int, int]:
 93        - int: 判定後の局数
 94        - int: 判定後の連チャン数
 95        - int: 次の親
 96
 97    """
 98    flg: bool = False
 99    if winners:
100        flg = parent in winners  # 和了時: 親が和了していれば連チャン
101    elif tenpai:
102        flg = tenpai[parent]  # 流局時: 親がテンパイしていれば連チャン
103
104    if flg:
105        renchan_count += 1
106    else:
107        parent = (parent + 1) % 4
108        renchan_count = 0
109        total_rounds += 1
110
111    return (total_rounds, renchan_count, parent)
112
113
114def simulate_game() -> list[int]:
115    """ゲーム進行シミュレーション"""
116    scores: list[int] = [INITIAL_POINTS] * 4  # 配給原点(0:東家 1:南家 2:西家 3:北家)
117    parent: int = 0  # 親番
118    total_rounds: int = 0  # 局数
119    renchan_count: int = 0  # 本場
120
121    while total_rounds < MAX_ROUNDS:
122        member = list(range(4))  # 0:東家 1:南家 2:西家 3:北家
123        wins = [random.random() < 0.23 for _ in member]  # 和了判定
124        num_wins = sum(wins)
125
126        if num_wins in [2, 3] and random.random() < 0.01:  # ダブロン発生
127            winners, losers = determine_winner(num_wins)
128            discarder = random.choice(losers)  # 放銃役
129
130            for winner in winners:
131                is_parent = winner == parent
132                point = determine_point(is_parent, False)
133                assert isinstance(point, int), "point should be a int"
134                scores[winner] += point + 300 * renchan_count
135                scores[discarder] -= point + 300 * renchan_count
136
137            total_rounds, renchan_count, parent = should_renchan(winners, parent, [], total_rounds, renchan_count)
138
139        elif num_wins in [1, 2, 3]:  # 通常の和了処理
140            winners, losers = determine_winner(1)
141            winner = winners[0]  # 和了役
142            is_parent = winner == parent  # 和了役が親か?
143
144            if random.random() > 0.75:  # ツモによる点数移動
145                point_data = determine_point(is_parent, True)
146                assert isinstance(point_data, tuple), "point_data should be a tuple"
147                for i in losers:
148                    pay = point_data[1] if i == parent else point_data[0]
149                    scores[i] -= pay + 100 * renchan_count
150                    scores[winner] += pay + 100 * renchan_count
151            else:  # 被ツモによる点数移動
152                discarder = random.choice(losers)  # 放銃役
153                pay = determine_point(is_parent, False)  # type: ignore[assignment]
154                assert isinstance(pay, int), "pay should be an int"
155                scores[discarder] -= pay + 300 * renchan_count
156                scores[winner] += pay + 300 * renchan_count
157
158            total_rounds, renchan_count, parent = should_renchan(winners, parent, [], total_rounds, renchan_count)
159
160        else:  # 流局処理
161            tenpai = [random.random() < 0.25 for _ in member]
162            noten = [not x for x in tenpai]
163            payment = []
164
165            if not (all(tenpai) or all(noten)):
166                for i in tenpai:
167                    if i:
168                        payment.append(int(3000 / sum(tenpai)))
169                    else:
170                        payment.append(int(-3000 / sum(noten)))
171
172                for i in member:
173                    scores[i] += payment[i]
174
175            total_rounds, renchan_count, parent = should_renchan([], parent, tenpai, total_rounds, renchan_count)
176
177        if any(score < 0 for score in scores):
178            break
179
180    return scores
181
182
183if __name__ == "__main__":
184    final_scores = simulate_game()
185    print("最終素点:", final_scores, "素点合計", sum(final_scores))
INITIAL_POINTS: int = 25000
MAX_ROUNDS: int = 8
HAN_POINTS: dict[int, dict[str, int | tuple[int, ...]]] = {1: {'ron_child': 1300, 'ron_parent': 2000, 'tsumo_child': (300, 500), 'tsumo_parent': (500,)}, 2: {'ron_child': 2600, 'ron_parent': 3900, 'tsumo_child': (500, 1000), 'tsumo_parent': (1000,)}, 3: {'ron_child': 5200, 'ron_parent': 7700, 'tsumo_child': (1000, 2000), 'tsumo_parent': (2000,)}, 4: {'ron_child': 8000, 'ron_parent': 12000, 'tsumo_child': (2000, 4000), 'tsumo_parent': (4000,)}, 5: {'ron_child': 8000, 'ron_parent': 12000, 'tsumo_child': (2000, 4000), 'tsumo_parent': (4000,)}, 6: {'ron_child': 12000, 'ron_parent': 18000, 'tsumo_child': (3000, 6000), 'tsumo_parent': (6000,)}, 7: {'ron_child': 12000, 'ron_parent': 18000, 'tsumo_child': (3000, 6000), 'tsumo_parent': (6000,)}, 8: {'ron_child': 16000, 'ron_parent': 24000, 'tsumo_child': (4000, 8000), 'tsumo_parent': (8000,)}, 9: {'ron_child': 16000, 'ron_parent': 24000, 'tsumo_child': (4000, 8000), 'tsumo_parent': (8000,)}, 10: {'ron_child': 24000, 'ron_parent': 36000, 'tsumo_child': (6000, 12000), 'tsumo_parent': (12000,)}, 11: {'ron_child': 24000, 'ron_parent': 36000, 'tsumo_child': (6000, 12000), 'tsumo_parent': (12000,)}, 12: {'ron_child': 24000, 'ron_parent': 36000, 'tsumo_child': (6000, 12000), 'tsumo_parent': (12000,)}, 13: {'ron_child': 32000, 'ron_parent': 48000, 'tsumo_child': (8000, 16000), 'tsumo_parent': (16000,)}, 14: {'ron_child': 32000, 'ron_parent': 48000, 'tsumo_child': (8000, 16000), 'tsumo_parent': (16000,)}, 15: {'ron_child': 64000, 'ron_parent': 96000, 'tsumo_child': (16000, 32000), 'tsumo_parent': (32000,)}}
def determine_point(is_parent: bool, is_tsumo: bool) -> int | tuple[int, ...]:
38def determine_point(is_parent: bool, is_tsumo: bool) -> int | tuple[int, ...]:
39    """
40    和了打点を決める
41
42    Args:
43        is_parent (bool): 親フラグ
44        is_tsumo (bool): ツモ/被ツモフラグ
45
46    Returns:
47        int | tuple: 打点
48
49    """
50    rank = 1
51    while rank < 15:
52        success_prob = max([0, 0.6 - 0.02 * rank])
53        if random.random() > success_prob:
54            break
55        rank += 1
56
57    key = "tsumo_parent" if is_parent and is_tsumo else "tsumo_child" if not is_parent and is_tsumo else "ron_parent" if is_parent else "ron_child"
58
59    return HAN_POINTS[rank][key]

和了打点を決める

Arguments:
  • is_parent (bool): 親フラグ
  • is_tsumo (bool): ツモ/被ツモフラグ
Returns:

int | tuple: 打点

def determine_winner(k: int) -> tuple[list[int], list[int]]:
62def determine_winner(k: int) -> tuple[list[int], list[int]]:
63    """
64    和了役を抽選し、放銃役候補と分けてリストを返す
65
66    Args:
67        k (int): 和了役に選ばれる人数
68
69    Returns:
70        tuple[list[int], list[int]]: 抽選結果
71
72    """
73    member: list[int] = list(range(4))
74    winners: list[int] = random.sample(member, k=k)  # 和了役
75    losers: list[int] = [i for i in member if i not in winners]
76
77    return (winners, losers)

和了役を抽選し、放銃役候補と分けてリストを返す

Arguments:
  • k (int): 和了役に選ばれる人数
Returns:

tuple[list[int], list[int]]: 抽選結果

def should_renchan( winners: list[int], parent: int, tenpai: list[bool], total_rounds: int, renchan_count: int) -> tuple[int, int, int]:
 80def should_renchan(winners: list[int], parent: int, tenpai: list[bool], total_rounds: int, renchan_count: int) -> tuple[int, int, int]:
 81    """
 82    連チャンの判定を行う
 83
 84    Args:
 85        winners (list[int]): 和了者のリスト(流局時は空リスト)
 86        parent (int): 現在の親
 87        tenpai (list[bool]): 流局時のテンパイ状況(和了時は空リスト)
 88        total_rounds (int): 現在の局数
 89        renchan_count (int): 現在の連チャン数
 90
 91
 92    Returns:
 93        tuple[int, int, int]:
 94        - int: 判定後の局数
 95        - int: 判定後の連チャン数
 96        - int: 次の親
 97
 98    """
 99    flg: bool = False
100    if winners:
101        flg = parent in winners  # 和了時: 親が和了していれば連チャン
102    elif tenpai:
103        flg = tenpai[parent]  # 流局時: 親がテンパイしていれば連チャン
104
105    if flg:
106        renchan_count += 1
107    else:
108        parent = (parent + 1) % 4
109        renchan_count = 0
110        total_rounds += 1
111
112    return (total_rounds, renchan_count, parent)

連チャンの判定を行う

Arguments:
  • winners (list[int]): 和了者のリスト(流局時は空リスト)
  • parent (int): 現在の親
  • tenpai (list[bool]): 流局時のテンパイ状況(和了時は空リスト)
  • total_rounds (int): 現在の局数
  • renchan_count (int): 現在の連チャン数
Returns:

tuple[int, int, int]:

  • int: 判定後の局数
  • int: 判定後の連チャン数
  • int: 次の親
def simulate_game() -> list[int]:
115def simulate_game() -> list[int]:
116    """ゲーム進行シミュレーション"""
117    scores: list[int] = [INITIAL_POINTS] * 4  # 配給原点(0:東家 1:南家 2:西家 3:北家)
118    parent: int = 0  # 親番
119    total_rounds: int = 0  # 局数
120    renchan_count: int = 0  # 本場
121
122    while total_rounds < MAX_ROUNDS:
123        member = list(range(4))  # 0:東家 1:南家 2:西家 3:北家
124        wins = [random.random() < 0.23 for _ in member]  # 和了判定
125        num_wins = sum(wins)
126
127        if num_wins in [2, 3] and random.random() < 0.01:  # ダブロン発生
128            winners, losers = determine_winner(num_wins)
129            discarder = random.choice(losers)  # 放銃役
130
131            for winner in winners:
132                is_parent = winner == parent
133                point = determine_point(is_parent, False)
134                assert isinstance(point, int), "point should be a int"
135                scores[winner] += point + 300 * renchan_count
136                scores[discarder] -= point + 300 * renchan_count
137
138            total_rounds, renchan_count, parent = should_renchan(winners, parent, [], total_rounds, renchan_count)
139
140        elif num_wins in [1, 2, 3]:  # 通常の和了処理
141            winners, losers = determine_winner(1)
142            winner = winners[0]  # 和了役
143            is_parent = winner == parent  # 和了役が親か?
144
145            if random.random() > 0.75:  # ツモによる点数移動
146                point_data = determine_point(is_parent, True)
147                assert isinstance(point_data, tuple), "point_data should be a tuple"
148                for i in losers:
149                    pay = point_data[1] if i == parent else point_data[0]
150                    scores[i] -= pay + 100 * renchan_count
151                    scores[winner] += pay + 100 * renchan_count
152            else:  # 被ツモによる点数移動
153                discarder = random.choice(losers)  # 放銃役
154                pay = determine_point(is_parent, False)  # type: ignore[assignment]
155                assert isinstance(pay, int), "pay should be an int"
156                scores[discarder] -= pay + 300 * renchan_count
157                scores[winner] += pay + 300 * renchan_count
158
159            total_rounds, renchan_count, parent = should_renchan(winners, parent, [], total_rounds, renchan_count)
160
161        else:  # 流局処理
162            tenpai = [random.random() < 0.25 for _ in member]
163            noten = [not x for x in tenpai]
164            payment = []
165
166            if not (all(tenpai) or all(noten)):
167                for i in tenpai:
168                    if i:
169                        payment.append(int(3000 / sum(tenpai)))
170                    else:
171                        payment.append(int(-3000 / sum(noten)))
172
173                for i in member:
174                    scores[i] += payment[i]
175
176            total_rounds, renchan_count, parent = should_renchan([], parent, tenpai, total_rounds, renchan_count)
177
178        if any(score < 0 for score in scores):
179            break
180
181    return scores

ゲーム進行シミュレーション