Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

まじかる☆ベーカリーのパンはどれほどの確率で焼けるのか?

www.youtube.com

こちらは僕が好きでよく見ている動画です。まじかる☆ベーカリーというボードゲームで遊ぶ動画です。

このゲームは「パン」を焼き上げて得点をもらうゲームとなっており、パンにはそれぞれ焼き上げるための条件が書かれています。たとえばコッペパンは「サイコロを2つ振って合計8以上」なら焼き上げ成功となります。

しかし、動画を見ているうちにふと「あれ?確率がどれも1/2以下なのでは?」と気になってしまったのでプログラミングをして計算してみました。

計算方法

愚直に「2つ(or 3つ)のサイコロからあり得る目」のパターンを列挙し、条件に一致するパターンの数を数えます。サイコロが3つでも高々216通りしか出目はないので、そこまで大変な計算にならないとカンタンに想像できます。

パンの条件一覧

パンの条件はこのようになっています。

パン 焼き上げ条件
コッペパン 2つ振って合計8以上
バターロール 2つ振ってそれぞれ3以上
フランスパン 3つ振って合計13以上
マフィン 2つ振って合計5以下
チョココロネ 2つ振って6、6を出す
ベーグル 3つ振ってそれぞれ5以上
シュトレン 3つ振ってすべて同じ目を出す
ブリオッシュ 3つ振って連続する目をだす(123や345)
エッグベネディクト 3つ振って666か111

計算結果

検証に使ったコードは最後に掲載します。

結果はこちらになります。

パン 焼き上げられる確率(%)
コッペパン 41.667%
バターロール 44.444%
フランスパン 25.926%
マフィン 27.778%
チョココロネ 2.778%
ベーグル 3.704%
シュトレン 2.778%
ブリオッシュ 11.111%
エッグベネディクト 0.926%

10%以下が4つ、エッグベネディクトに至っては1%を切っているなどかなり凶悪ですね…コッペパン、バターロール以外は自力で1回で焼き上げるのはかなり至難の技であるとわかります。

検証コード

まず、サイコロがn個のときのサイコロのパターンを列挙します。

const dice = (n) => {
    if (n == 1)
        return [[1], [2], [3], [4], [5], [6]];
    const ans = [];
    for (const item of dice(n-1)) {
        for (let i = 1; i <= 6; ++i) {
            ans.push(item.concat(i));
        }
    }
    return ans;
}

こんな感じで列挙できます。

[
  [ 1, 1 ], [ 1, 2 ], [ 1, 3 ], [ 1, 4 ],
  [ 1, 5 ], [ 1, 6 ], [ 2, 1 ], [ 2, 2 ],
  [ 2, 3 ], [ 2, 4 ], [ 2, 5 ], [ 2, 6 ],
  [ 3, 1 ], [ 3, 2 ], [ 3, 3 ], [ 3, 4 ],
  [ 3, 5 ], [ 3, 6 ], [ 4, 1 ], [ 4, 2 ],
  [ 4, 3 ], [ 4, 4 ], [ 4, 5 ], [ 4, 6 ],
  [ 5, 1 ], [ 5, 2 ], [ 5, 3 ], [ 5, 4 ],
  [ 5, 5 ], [ 5, 6 ], [ 6, 1 ], [ 6, 2 ],
  [ 6, 3 ], [ 6, 4 ], [ 6, 5 ], [ 6, 6 ]
]

次に各パンの条件を関数として表していきます。

const sum_ge = n => (pattern) => pattern.reduce((acc, n) => acc + n) >= n; // 与えられたパターンの合計がn以上か?
const each_ge = n => (pattern) => pattern.every(m => m >= n); // 与えられたパターンのそれぞれの目がn以上か?

このような感じで作っていきます。

最後に実際に計算する部分を書いて完成です。

全体

// 条件関数
const sum_ge = n => (pattern) => pattern.reduce((acc, n) => acc + n) >= n;
const sum_le = n => (pattern) => pattern.reduce((acc, n) => acc + n) <= n;
const each_ge = n => (pattern) => pattern.every(m => m >= n);
const each_le = n => (pattern) => pattern.every(m => m <= n);
const pair = (n, m) => (pattern) => JSON.stringify(pattern.sort()) == JSON.stringify([n, m].sort());
const same = () => (pattern) => new Set(pattern).size == 1;
const sequencial = () => (pattern) => {
    pattern.sort();
    let isOK = true;
    for (let i = 0; i < pattern.length - 1; ++i)
        if (pattern[i+1] != pattern[i] + 1)
            isOK = false;
    return isOK;
};
const egg = () => (pattern) => new Set(pattern).size == 1 && (new Set(pattern).has(1) || new Set(pattern).has(6));

// 各パンの設定
const bread = {
    // パンの名前: [振るサイコロの数, 条件]
    コッペパン: [2, sum_ge(8)],
    バターロール: [2, each_ge(3)],
    フランスパン: [3, sum_ge(13)],
    マフィン: [2, sum_le(5)],
    チョココロネ: [2, pair(6,6)],
    ベーグル: [3, each_ge(5)],
    シュトレン: [3, same()],
    ブリオッシュ: [3, sequencial()],
    エッグベネディクト: [3, egg()],
};

const dice = (n) => {
    // n個のサイコロを振った際に出るパターンを列挙する
    if (n == 1)
        return [[1], [2], [3], [4], [5], [6]];
    const ans = [];
    for (const item of dice(n-1)) {
        for (let i = 1; i <= 6; ++i) {
            ans.push(item.concat(i));
        }
    }
    return ans;
}
for (const [name, cond] of Object.entries(breabread)) {
    let count = 0; // 条件を満たしたパターンの数
    const cases = dice(cond[0]);
    for (const item of cases) {
        if (cond[1](item))
            ++count;
    }
    console.log(name, count, cases.length, count / cases.length * 100);
}