年の差ラジオのWOC
https://radio.rcc.jp/toshinosa/
ザ・ギース尾関高文とOCHA NORMA広本瑠璃の年の差ラジオをでゲームをやっていましたが、全く勝てそうになかったのに勝ちました。勝つ確率はどれくらいなのか算出します。
ルールとしては明確ではなかったですが、プレーヤー1は1イニングに1~20のどれかの点数が同確率で入る、プレーヤー2はアウトが1/2、単打が1/3、ホームランが1/6で起こるとします。イニングが終わって10点以上差がついていたらコールドです。
まず、ランダムに1億回プレイしてみましょう。
from __future__ import annotations from itertools import * from collections import Counter import random import sys from enum import Enum from typing import List, Tuple State = Tuple[int, int, int] class Action(Enum): OUT = 1 HIT = 2 HOMERUN = 3 @staticmethod def select_randomly(): r = random.randrange(1, 7) if r <= 3: return Action.OUT elif r <= 5: return Action.HIT else: return Action.HOMERUN def transit(s: State, a: Action) -> State: (score, out, base) = s if a == Action.OUT: return (score, out + 1, base) elif a == Action.HIT: if base == 3: return (score + 1, out, base) else: return (score, out, base + 1) else: return (score + base + 1, out, 0) def play1_randamly() -> int: return random.randrange(1, 21) def play2_randomly() -> int: s = (0, 0, 0) while True: s = transit(s, Action.select_randomly()) if s[1] == 3: return s[0] def play_randomly() -> str: score1 = 0 score2 = 0 for _ in range(9): score1 += play1_randamly() score2 += play2_randomly() if score1 >= score2 + 10: return 'Lose' elif score2 >= score1 + 10: return 'Win' if score1 > score2: return 'Lose' elif score1 < score2: return 'Win' else: return 'Draw' N = int(sys.argv[1]) c = Counter(play_randomly() for _ in range(N)) print(c)
Counter({'Lose': 99860253, 'Win': 139604, 'Draw': 143})
勝つ確率は0.14%程度のようです。
次に、真面目に確率計算をします。最初にプレーヤー2が1イニングに何点入るかの確率を求めます。ただし、181点以上入ったら必ず勝つので、181点以上はまとめて181点とします。
from __future__ import annotations from functools import reduce from itertools import * from collections import Counter import random import sys from enum import Enum from typing import List, Tuple State = Tuple[int, int, int] class Action(Enum): OUT = 1 HIT = 2 HOMERUN = 3 @staticmethod def probability(a: Action): if a == Action.OUT: return 3 / 6 elif a == Action.HIT: return 2 / 6 else: return 1 / 6 def transit(s: State, a: Action) -> State: (score, out, base) = s if a == Action.OUT: return (score, out + 1, base) elif a == Action.HIT: if base == 3: return (score + 1, out, base) else: return (score, out, base + 1) else: return (score + base + 1, out, 0) def probs_inning(N: int) -> list[float]: probs: list[float] = [0.0] * (N + 2) c: Dict[State, float] = Counter() c[(0, 0, 0)] = 1.0 while c: new_c: Dict[State, float] = Counter() for s0, p0 in c.items(): for action in Action: s1 = transit(s0, action) score, out, base = s1 p1 = p0 * Action.probability(action) if out == 3: probs[score] += p1 elif score > N: pass else: new_c[s1] += p1 c = new_c print(1.0 - sum(probs[:11])) # 11点以上取る確率 probs[N+1] = probs[N] # 半分ずつ減っていくのでたぶんこれくらい return probs def decide_probs(): probs1 = [0.0] + [1/20] * 20 probs2 = probs_inning(180) # 181点上なら必ず勝つ probs = [0.0] * 3 # [Lose, Win, Draw] scores = { (0, 0): 1.0 } for i in range(9): new_scores: Dict[tuple[int, int], float] = Counter() for (score1, score2), p0 in scores.items(): for s1, s2 in product(range(1, 21), range(182)): prob1 = probs1[s1] prob2 = probs2[s2] new_score1 = score1 + s1 new_score2 = score2 + s2 p = p0 * prob1 * prob2 if new_score1 >= new_score2 + 10: probs[0] += p # Lose elif new_score2 >= new_score1 + 10: probs[1] += p # Win else: new_scores[(new_score1, new_score2)] += p scores = new_scores for (score1, score2), p in scores.items(): if score1 > score2: probs[0] += p # Lose elif score1 < score2: probs[1] += p # Win else: probs[2] += p # Draw print("Lose: %.6f, Win: %.6f, Draw: %.2e" % tuple(probs)) decide_probs()
まず、プレーヤー2が1イニングに11点以上取る確率は、0.6%程度です。
そして、
Lose: 0.998606, Win: 0.001392, Draw: 1.39e-06
ランダムにプレイしたときとほとんど変わらないですね。