エクスペディションズは何ターンで終わるのか?
はじめに
エクスペディションズというゲームは大体何ターン程度で終わるゲームなのだろうか?
このゲームはあらかじめ決められたターン数をこなすゲームではない。誰かがゲームの終了条件を満たすまでつづくゲームだ。
そのため各ゲームが何ターンで終わるかはやってみるまでわからないが、何ターン程度で終わるのかは戦略を練る上で見積もっておきたい。
各プレイでゲーム終了時までにかかったターン数を集計してもよいが、統計的に有意なプレイ回数をこなすのは現実的ではない。
そこで今回は、開発側が想定しているゲーム終了ターンを求めるために1人用ルールであるオートマを参考にすることにした。
目次
オートマとは
オートマとはエクスペディションズに搭載されている1人用ゲームモードである。
オートマは14種類ある行動カードに記載された内容にしたがって各ターンプレイをおこなう。対人戦と同様に誰かが規定の数の栄光を置ききればゲームは終了へと向かう。
オートマは2つのメックを有しており、同様に栄光も4つではなく8つまで置くことができる。栄光の置き方はプレイヤーと異なり、単に規定のターン数を経ることで栄光を達成していける。
オートマカードにはすごろくのようなマス目がある。1ターンごとにマスを進んでいき、星の書かれているマスに到達すると栄光を1つ置く。
ただし行動カードの上部に青い四角のマークがなければマスを進むことはない。マークのないカードは14枚中3枚存在する。オートマの栄光進行度は、これによって多少のランダム性を有している。
オートマの行動カードは、ゲーム開始時に14枚の中から無作為に2枚取り除かれて計12枚の山札からはじまる。1ターン毎に山札から1枚めくっていき、山札がなくなったら取り除かれた2枚も加えて14枚の山札を作成する。以降はこれの繰り返しだ。
厳密には以下のような手順となる。
- 山札から無作為に2枚のカードを取り除く。
- 残り12枚の山札からカードを1枚ずつ公開する。
- 12枚のカードを公開し終わったら、手順1で取り除いたカード2枚と公開した12枚のカードを混ぜて再度14枚の山札を作成する。
- 14枚の山札からカードを1枚ずつ公開する。
- 14枚のカードを公開し終わったら、公開した14枚のカードを混ぜて再度14枚の山札を作成する。
- 手順4に戻る。
たとえばレベル3のオートマは23マス目で8つ目の栄光を置ける。青いマークのある行動カードをが23枚公開されたらゲームが終了に向かうということだ。
このときプレイヤーは何ターンゲームをプレイできるのだろうか? それにはマークのありなし関係なく、めくられたカードの合計枚数を求める必要がある。
ChatGPTに聞いてみる
行動カードにマークの有無によるランダム性があることから、終了ターンが一つに定まらないことは明らかだ。おそらく何%で何ターンといった形で答えが表されるはずだ。
確率計算について考えるのは面倒なので、まずはChatGPTに丸投げすることにした。
こんな感じで聞いてみる。
山札:1と書かれたカードが11枚、0と書かれたカードが3枚からなる14枚のカードの束
以下の手順で数字を合計する。
1.山札から無作為に2枚のカードを取り除く。
2.残り12枚の山札からカードを1枚ずつ公開し、公開したカードに書かれた数値を合計していく。
3.12枚のカードを公開し終わったら、手順1で取り除いたカード2枚と公開した12枚のカードを混ぜて再度14枚の山札を作成する。
4.14枚の山札からカードを1枚ずつ公開し、公開したカードに書かれた数値を合計していく。
5.14枚のカードを公開し終わったら、公開した14枚のカードを混ぜて再度14枚の山札を作成する。
6.手順4に戻る。このような手順をおこなうとき、合計した数値が19以上となった時点で公開されたカードの枚数は何枚か、またその枚数の確率分布を回答しなさい。
1と書かれたカードがマークのあるカードのことで0がマークのないカードのことだ。
この問いに対する最初の回答がこちら
この問題は非常に複雑であり、手順ごとの合計値がどのようになるかを直接計算するのは難しいですが、シミュレーションを通じて確率的にアプローチできます。
以下は、シミュレーションに基づく結果の一例です。この結果は乱数に基づくものであり、実際の確率とは異なる可能性があります。
シミュレーション回数: 100,000回 合計が19以上になった場合の公開されたカードの枚数の分布確率: 1枚目: 約 19.9% 2枚目: 約 39.6% 3枚目: 約 29.5% 4枚目: 約 10.9%
このシミュレーションでは、合計が19以上になる場合、それが公開されたカードの枚数ごとの分布を示しています。ただし、これはあくまで推定であり、シミュレーション回数を増やすことでより正確な結果が得られる可能性があります。
この結果をもとに、合計が19以上になる場合、2枚目のカードが公開される確率が最も高いことが分かります。
全く意味がわからない。
4枚めくろうが合計は最大4にしかならないのだから明らかになにかを勘違いしている。
ここで諦めずにどういう理屈か聞いてみることにした。
2枚めとはどういう意味ですか?
2枚目のカードが公開された時点では、最大でも1+1で合計値が2にしかなりません
申し訳ありません。確かにその点が不明瞭でした。再度説明いたします。
山札から2枚のカードが取り除かれ、その後残りの12枚から数値を合計していく手順を考えています。この場合、合計が19以上になるのは、最初に取り除かれた2枚の中に1が含まれており、かつ、残りの12枚での合計が18以上である場合です。
したがって、合計が19以上となる時点で公開されているのは、最初に取り除かれた2枚の中で1が含まれており、その後の手順で残りの12枚で合計が19以上になる場合です。この時点で公開されるのは、最初に取り除かれた2枚目のカードです。
具体的には、最初に1と0が取り除かれ、残りの12枚で合計が19以上になる場合、その後の手順で最初に取り除かれた2枚目のカードが公開され、その時点で合計が19以上になります。
再度誤解を招いた点についてお詫び申し上げます。おっしゃる通り、2枚目のカードが公開された時点では最大で合計値は2にしかなりません。合計が19以上になるのは、その後の手順での合計が影響しています。
何を勘違いしているのか残りの12枚で合計が19以上になるケースが存在すると思っているらしい。
私はいったんここで会話を諦めて別のアプローチを取ることにした。
モンテカルロ法
モンテカルロ法(Monte Carlo method)とは、乱数を用いてシミュレーションや数値計算を行う手法です。求める値が複数の確率変数の関数で表現できる場合に、各変数で仮定される確率分布に沿った標本値を乱数を利用して大量に生成して、演算を多数回試行し、その結果から求める値の分布を得る方法です。
Google検索の生成AIより
要は試行回数稼いで出てきた割合を用いる手法だ。
オートマ相手でもそんなにたくさんプレイはできないため、行動カードの出方をプログラミングしてシミュレーションする。
というわけで以下のようなプログラムを用意した。
from random import shuffle from collections import Counter # 求めたいマス目の一覧 PATTERN = [23] # 試行回数 NUM_OF_ATTEMPTS = 1000000 def main(): for i in PATTERN: print(f'====={str(i)}======') simulate(i) def simulate(value): c = Counter([deck_simulate(value) for i in range(1, NUM_OF_ATTEMPTS)]) for k in sorted(c.keys()): print(f"{k}: {round(c[k] / NUM_OF_ATTEMPTS * 100, 2)}%") def deck_simulate(value): # 1週目のデッキ first_deck = shuffle_deck() first_deck.pop(0) first_deck.pop(0) total = sum(first_deck) turns = len(first_deck) # 2週目以降のデッキ deck = shuffle_deck() while total < value: turns += 1 total += deck.pop() if len(deck) == 0: # カードが尽きた場合、山札を再構築 deck = shuffle_deck() return turns def shuffle_deck(): deck = 3*[0] + 11*[1] shuffle(deck) return deck if __name__ == '__main__': main()
ポイントは最初の数行のみで、とりあえず23マス目に到達するまでのシミュレーションを100万回おこなうことさえわかってもらえればよい。
これを実行すると以下のような答えが返ってくる。
=====23=====
27: 2.62%
28: 22.49%
29: 38.38%
30: 25.44%
31: 9.4%
32: 1.67%
これの意味するところは、23マス目に到達するまで――レベル3のオートマが8つ目の栄光を置くまで――にかかるターン数は27~32ターンの間であるということである。そのうち29ターン目に達成される確率が約38.38%で最も高いということだ。
求め方の性質上厳密な確率でないが、おおまかな終了ターン数を見積もる分には十分だろう。
この方法で3, 4, 5レベルのオートマが6, 7, 8つ目の栄光を達成するまでにかかるターン数を求めた。
結果
レベル | 6栄光(19) | 7栄光(21) | 8栄光(23) | |||
---|---|---|---|---|---|---|
ターン数 | 確率 | ターン数 | 確率 | ターン数 | 確率 | |
3 | 20 | 0.18% | 22 | 0.04% | 27 | 2.62% |
21 | 1.73% | 23 | 0.36% | 28 | 22.49% | |
22 | 7.30% | 24 | 2.08% | 29 | 38.38% | |
23 | 19.48% | 25 | 8.58% | 30 | 25.44% | |
24 | 34.80% | 26 | 28.50% | 31 | 9.40% | |
25 | 36.52% | 27 | 47.44% | 32 | 1.67% | |
28 | 10.99% | |||||
29 | 1.82% | |||||
30 | 0.17% | |||||
レベル | 6栄光(17) | 7栄光(19) | 8栄光(20) | |||
ターン数 | 確率 | ターン数 | 確率 | ターン数 | 確率 | |
4 | 18 | 0.51% | 20 | 0.18% | 21 | 0.09% |
19 | 4.64% | 21 | 1.73% | 22 | 0.87% | |
20 | 14.90% | 22 | 7.30% | 23 | 4.39% | |
21 | 27.71% | 23 | 19.48% | 24 | 14.28% | |
22 | 32.37% | 24 | 34.80% | 25 | 32.86% | |
23 | 19.88% | 25 | 36.52% | 26 | 47.50% | |
レベル | 6栄光(16) | 7栄光(18) | 8栄光(19) | |||
ターン数 | 確率 | ターン数 | 確率 | ターン数 | 確率 | |
5 | 17 | 0.78% | 19 | 0.32% | 20 | 0.18% |
18 | 6.81% | 20 | 2.95% | 21 | 1.73% | |
19 | 19.36% | 21 | 10.80% | 22 | 7.30% | |
20 | 30.31% | 22 | 24.04% | 23 | 19.48% | |
21 | 28.81% | 23 | 34.40% | 24 | 34.80% | |
22 | 13.93% | 24 | 27.49% | 25 | 36.52% |
ざっと並べてみたがどの部分を見ればよいのだろうか。
オートマとの実力が拮抗している前提ならば、プレイヤーの4栄光にあたる8栄光におけるターン数を一つの目安としてみるのがいいだろう。この場合、レベル3のオートマ相手では約85%で28~30ターンで終了条件が満たされる。
対人戦を意識するなら、誰かが3つ目の栄光を置いたあたりで自分が4つ目の栄光を置くことを想定してみよう。その場合はオートマが6つ目か7つ目の栄光を置くターンを参照するのがよさそうだ。
オートマの中間レベルであるレベル3が、6つ目の栄光を達成するのは9割方23~25ターンで、7つ目は25~28ターンが大方の流れとなるだろう。したがって、このゲームはだいたい25ターン前後を念頭にゲームプランを組んでおけば、大きな間違いはないといえるのではないだろうか。
もちろん実際にオートマ戦をする場合は、ほぼこの数字にしたがってゲームが進行していくので参考にしてほしい。
余談
後日再度ChatGPTに同じ質問をしたところ以下のようなコードを返してきてくれた。
import random def simulate_deck(): deck = [1] * 11 + [0] * 3 # シャッフルして山札を作成 random.shuffle(deck) # 初めて合計が23以上になるまで繰り返す total = 0 draws = 0 while total < 23: draws += 1 total += deck.pop() if len(deck) == 0: # カードが尽きた場合、山札を再構築 deck = [1] * 11 + [0] * 3 random.shuffle(deck) return draws def monte_carlo_simulation(iterations): results = [] for _ in range(iterations): results.append(simulate_deck()) return results # シミュレーションの実行 simulation_results = monte_carlo_simulation(100000) # 結果の分析 count_over_23 = sum(1 for result in simulation_results if result >= 23) probability_over_23 = count_over_23 / len(simulation_results) print(f"合計が23以上となる確率: {probability_over_23:.4f}")
これは23になるかどうかの確率を求めているもので、もちろんカードをめくり続けていればいずれは23を超えるので確率は必ず100%となる。
シミュレーション自体は合っているが、出してる回答の方向性が異なり若干惜しい。