年の差ラジオの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

ランダムにプレイしたときとほとんど変わらないですね。