こちらは僕が好きでよく見ている動画です。まじかる☆ベーカリーというボードゲームで遊ぶ動画です。
このゲームは「パン」を焼き上げて得点をもらうゲームとなっており、パンにはそれぞれ焼き上げるための条件が書かれています。たとえばコッペパンは「サイコロを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); }