クリーチャーVS除去にChatGPTを使って挑んでみた
除去とクリーチャーはどっちが大事なんだ?!
多次元宇宙の古典曰く、除去はクリーチャーより優先すべし。
除去は相手のクリーチャーと交換できるのでクリーチャーの代わりになる。だがクリーチャーは除去の代わりになるかわからない。
だがクリーチャーがない全部除去だけのデッキって勝たなくないか?
クリーチャーだけのデッキなら勝つかもしれないからクリーチャーやっぱ除去より大事じゃね?
なんていうのは誤った二者択一である。
実際は、ある程度の枚数の除去とクリーチャーが入ったデッキがいいはず。除去を入れすぎると、もっとクリーチャーを入れた方がよくなり、除去が少なすぎると除去をもっと入れたほうがよくなるはず。
となると、そのターニングポイントを知りたくなるもの。
どっかの地点で入れ替わる、そのどっかが知りたい!
逆に言えばこの入れ替わる点が目標とする最適点なので、知っておくとドラフトのピックなんかの参考にできそう。
マジックの特定の環境、となると難しすぎて答えが出ないので、以下のようなすごく簡略化したカードプールで考えてみる。
プールにあるのはオリカ2枚を含む3枚とする。デッキ枚数は60枚固定で同一カードは枚数制限なしに4枚を超えて入れても良いものとする。
①沼
②ご都合オリカ //Convenient Ori-Ca (3) クリーチャー
このクリーチャーは攻撃可能なら攻撃する。このクリーチャーはブロック可能ならブロックする。
3/3
③ご都合除去//Convenient Removal (1) ソーサリー
対象のクリーチャー一体を破壊する。
ChatGPTに聞いてみるぜ!
こんだけ簡略化したら普通にシミュレーションプログラム書けばよくね?と言うのは一旦置いといて・・・
ChatGPTは面白い。カードゲーマーに面白い使い方を考えてもらいたいので使ってみた過程を記す。
結果を言うと1時間ぐらい時短になった。
特に面白かったり新規性のある使い方はできていないが、プログラム調べながらなら書けるけどそんな得意じゃないという筆者レベルのコーダーや、興味あるけどよくわからんという人には便利だという感想。
では実際に聞いてみる・・・
その前に、ChatGPTくん数学わかるんか?
数理的推論能力が信用できるか試してみる。
カンパニーの当たり確率を一定以上にするためにクリーチャー何枚取ればいい?
これなら簡単に計算できるし、多くの人が知りたいことであろうからベンチマークに悪くない。
いざ
お!これなら高校の確率を覚えていない人でも計算できそうである。
でも、ここまで言ったなら具体的な数字出してくれよという感じである。
なんだ、出せるやんけ!
というか君、自分で問題解決のためのコードを書いて内部で実行する機能持ってたの?!すご!!シンギュラリティも目前か?!
ん・・・?ちょっと待て、24って・・・
正解は22なんだが・・・
試しにChatGPTが出してきたコードを実行してみると、22が出力される。
流石に内部でコードを実行する機構はないようだが、問題を出すとそれを解決するための手順を教えてくれたり、それを成立させるためのコードを書くところまではしてくれるようである。
ちなみに、ChatGPTは英語での精度のほうが良いらしいので、英語でも聞いてみた。一番賢いとされている4.0+英語である。
まるで計算したかのようにちょっと違う数字を教えてきた。
とんでもない。具体的な数字は当てにしないほうがいいし、間違っているものだと思ったほうがいい。
とはいえ、結構いい線行ってる数字ではある。
余談だが、これを見たとき群衆の英知の話を思い出した。
ChatGPTは現在最も多くの人の判断の集合をデータとして持っているシステムであるので、群衆の英知に頼りたかったらChatGPTに具体的な数字を聞いてみてもいいかもしれない…。
では、こんな感じのつもりで冒頭で述べた検証をしていく。
じゃあ改めて、聞いてみるぜ!
ちなみに "Let's think step by step." というのはお呪いである。ChatGPTある目的のために使用するとき、特定のキーワードを入れるとうまくいくというようなものがネットではお呪いと呼ばれている。
"Let's think step by step." (段階を追って考えよう)
は論理的な返答をして欲しいときのお呪いとして有名。
面倒だから英語のまま打ち込んでしまったところ…
返答が英語になってしまった・・・。
英語のが精度がいいので、
「まあいっか、このまま英語でやるか。後で適当に翻訳して記事にしよ」
と思っていたが
いや、翻訳してもらえばいいじゃん
つまり、
土地24、クリーチャー16、除去20ぐらいがいいぞ!
でもメタによるで、スタート地点にするだけにして自分で調整してや
ということらしい。
数理的な手順を踏んで何らかの解を出してくることが難しいのは、カンパニーのテストでわかっていた。
なのでこうなると
土地24、クリーチャー16、除去20は妥当な値に近いものか?(群衆の英知はGPTにあるか?)
それを確かめる術をGPTは提供してくれるか?
というのが新たな関心である。
(読まなくていい)
import random
# Define cards
SWAMP = "Swamp"
REMOVAL = "Removal"
CREATURE = "Creature"
def create_deck(num_swamps, num_removal, num_creatures):
return [SWAMP]*num_swamps + [REMOVAL]*num_removal + [CREATURE]*num_creatures
def simulate_game(deck_a, deck_b):
# Shuffle decks
random.shuffle(deck_a)
random.shuffle(deck_b)
# Implement a simple game simulation
# Return 1 if deck A wins, 0 if deck B wins
# This should be replaced with a more detailed simulation considering each card's effect
def run_simulation(num_games, deck_a, deck_b):
wins_a = 0
for _ in range(num_games):
result = simulate_game(deck_a[:], deck_b[:])
wins_a += result
win_rate_a = wins_a / num_games
return win_rate_a
deck_a = create_deck(24, 20, 16)
deck_b = create_deck(24, 20, 16)
num_games = 1000
win_rate_a = run_simulation(num_games, deck_a, deck_b)
print(f"Deck A win rate: {win_rate_a:.2%}")
(以下、肝心の対戦部分は自分で実装してやって内容が続く)
対戦部分が実装されてないのと、これだと手動で複数のデッキパターンを対戦させて検証しなければいけない。それは面倒。
なので残りのやることは
対戦部分の実装
複数プレイヤーを作って自動で対戦させてメタを回してどこに収束するか調べる
という二つ。
まず、前者、頑張ってChatGPTにシミュレータを書いてもらおうとしたが、諦めた・・・。
まず、「MTGのシミュレータ書いて」というと、MTGが複雑すぎるせいで、ちゃんと書いてくれない。適宜簡略化してもいいよと言っても、簡略化する部分が曖昧で動くコードを出してくれない。一個一個簡略化して言い要素を指定して言っても多すぎて書ききれない。
逆に、「こういうシミュレータ作って」と言って、
・初期手札は7枚
・先攻1T以外カードを引く
・土地があれば1T1枚置く
(以下略)
とすれば書いてくれそうではあるが、それをやるなら普通にプログラム書くのと手間が変わらないどころか、書いてもらったコードを読む手間が増える。
ということで仕方ないので自分で30分ぐらいで書いた。
次に後者。プレイヤーをたくさん対戦させて環境を回すために、遺伝的アルゴリズムでデッキを最適化するコード書いてとお願いした。
説明と一緒にこんなコードが出てきた。(読まなくていい)
import random
# Constants
GENERATIONS = 50
POPULATION_SIZE = 100
NUM_GAMES = 10
# Define cards
SWAMP = "Swamp"
REMOVAL = "Removal"
CREATURE = "Creature"
def create_deck(num_swamps, num_removal, num_creatures):
return [SWAMP]*num_swamps + [REMOVAL]*num_removal + [CREATURE]*num_creatures
def simulate_game(deck_a, deck_b):
# Implement a detailed game simulation as in the previous answer
pass
def create_initial_population(size):
population = []
for _ in range(size):
num_swamps = random.randint(20, 28)
num_removal = random.randint(10, 30)
num_creatures = 60 - num_swamps - num_removal
population.append(create_deck(num_swamps, num_removal, num_creatures))
return population
def select_survivors(population):
survivors = []
while len(survivors) < len(population):
a, b = random.sample(population, 2)
win_rate_a = run_simulation(NUM_GAMES, a, b)
if win_rate_a > 0.5:
survivors.append(a)
else:
survivors.append(b)
return survivors
def generate_offspring(survivors):
offspring = []
for _ in range(len(survivors) // 2):
parent_a, parent_b = random.sample(survivors, 2)
child = parent_a[:30] + parent_b[30:]
offspring.append(child)
return offspring
def run_genetic_algorithm():
population = create_initial_population(POPULATION_SIZE)
for generation in range(GENERATIONS):
survivors = select_survivors(population)
offspring = generate_offspring(survivors)
population = survivors + offspring
# Find the best individual in the final population
best_deck = max(population, key=lambda d: sum(run_simulation(NUM_GAMES, d, other) for other in population))
print(f"Optimal deck: {best_deck}")
run_genetic_algorithm()
ひな形に使えそう。遺伝的アルゴリズムについては知識として知っているだけで書いたことがなかったので自分でひな形を書いていたら30分ぐらいかかったと思う。30分時短。
途中経過を描画したかったので、こういうのも聞いてみる。
どんな感じに可視化されるか見たいので、ランダムに生存個体を選ぶようにして、シミュレーター部分がなくてもとりあえず動くものが出てくるのを期待。
出てきたものを・・・
import random
import matplotlib.pyplot as plt
class Individual:
def __init__(self, params):
self.params = params
def survive(self):
random.seed(self.params)
return random.random() > 0.5
def create_population(size):
return [Individual([random.random() for _ in range(3)]) for _ in range(size)]
def select_survivors(population):
return [ind for ind in population if ind.survive()]
def reproduce(parent1, parent2, noise_scale):
child_params = [(p1 + p2) / 2 + random.uniform(-noise_scale, noise_scale)
for p1, p2 in zip(parent1.params, parent2.params)]
return Individual(child_params)
def next_generation(population, noise_scale):
survivors = select_survivors(population)
while len(survivors) < len(population):
parent1 = random.choice(survivors)
parent2 = random.choice(survivors)
child = reproduce(parent1, parent2, noise_scale)
survivors.append(child)
return survivors
def simulate_genetic_algorithm(generations, population_size, noise_scale):
population = create_population(population_size)
history = [population]
for _ in range(generations - 1):
population = next_generation(population, noise_scale)
history.append(population)
return history
def plot_simulation(history):
plt.figure(figsize=(10, 6))
for i, param in enumerate(["Param 1", "Param 2", "Param 3"]):
plt.subplot(3, 1, i + 1)
plt.title(param)
plt.xlabel("Generation")
plt.ylabel("Value")
for gen in range(len(history)):
values = [ind.params[i] for ind in history[gen]]
plt.scatter([gen] * len(values), values, alpha=0.5)
plt.tight_layout()
plt.show()
if __name__ == "__main__":
generations = 50
population_size = 100
noise_scale = 0.1
history = simulate_genetic_algorithm(generations, population_size, noise_scale)
plot_simulation(history)
コピペすると・・・
動いた!
横軸が世代で各点が個体のパラメータである。世代と共に親世代のパラメータを引き継ぎながら少しランダムに変動しているのがわかる。
(個体数100のままだと点が点として見えづらかったので10に減らしている。)
多分描画のあれこれを調べて、動かしてみてをやっていたら30分ぐらいかかったと思うので計1時間時短。
改めて遺伝的アルゴリズムとか何言っとるかわからんという人のためにも、一旦今からやることを整理しながら細かい条件を説明。
以下のプールから最適なデッキを求めたい。
①沼
②ご都合オリカ //Convenient Ori-Ca (3) クリーチャー
このクリーチャーは攻撃可能なら攻撃する。このクリーチャーはブロック可能ならブロックする。
3/3
③ご都合除去//Convenient Removal (1) ソーサリー
対象のクリーチャー一体を破壊する。
自動で沢山対戦させる。ただし、それぞれの場面で最適な判断を均衡点出して計算するのはめんどくさすぎるので、簡単のため以下のような前提を置く。
①プレイヤーは絶対に土地を置けたらおく
②除去をクリーチャー展開より優先する
③除去もクリーチャーも使えたら使う
また、簡単のため2つの特殊ルールを置く。
①ライブラリアウトは引き分け扱い(先攻後攻だけで勝敗が決まるのは本質的でなう)
②手札の枚数制限は無限(捨てるカードを選ぶようにすると盤面から状況を判断しなければならないので実装が面倒)
これを最適化させるために、環境の変動を次のようにモデリングする。
①プレイヤーが100人いる。はじめ、これらのプレイヤーはランダムな配分のデッキを持っている。
②彼らは毎日ランダムに二人ペアを作って100回対戦する。
③勝ち越した50人は次の日も同じデッキを使う。
④負け越した50人は勝ち越した50人の中からランダムに二人を選び、その二人の平均となるデッキを選択する。
⑤これを100日繰り返す
ではいざ、切って貼って動かしてみる。
上から土地、クリーチャー、除去の順である。
土地12、クリーチャー19、除去29に収束した。
もう二回やってみたところ、それぞれ
土地11、クリーチャー19、除去30
土地10、クリーチャー19、除去31
になった。
それぐらいになるらしい。
何度も試行して結果を統計的に評価したり、結果の信頼度を数理的に評価したりは、飽きたし面倒なので、ではなく今回はやってみた系の記事なのでやらない。
やってみたい読者がChatGPTでTCGの問題に挑んでみた系記事を書いてくれると大変嬉しい。
結果だが、デッキに入ってるカードのマナコストが1と3のため土地枚数が10枚少しと、大幅に少なくなった。
ChatGPTの24枚という答えは、数字から推論したのではなく一般的に言われている24枚というのが現れてしまったので仕方ないと思うが。
除去とクリーチャーで除去のほうが枚数が多いというのは一応当たってはいる。
ちょっと条件を変えてみる。
①プレイヤーが100人いる。はじめ、これらのプレイヤーはランダムな配分のデッキを持っている。
②彼らは毎日ランダムに二人ペアを作って100回対戦する。
③勝ち越した50人は次の日も同じデッキを使う。
④負け越した50人のうち、40人は勝ち越した50人の中からランダムに二人を選び、その二人の平均となるデッキを選択する。
⑤負け越した50人のうちの残りの10人は完全にランダムなデッキを使用する
⑥これを100日繰り返す
ランダムにデッキを選ぶプレイヤーを追加。
これにより、もし土地10除去50で負けないことに特化したようなプレイヤーが現れると、土地5クリーチャー55でわずかに除去よりもクリーチャーが多いデッキなんかが現れて環境が動く…
というのは極端な例だが、局所最適解へ収束しないようにするとどうなるか観察してみる。(もともとの条件だと絶対どこかに収束する)
だいぶばらついた。この条件で土地が30ぐらいまで広がっているのはばらつきが大きすぎるのでランダムなデッキを選ぶ個体を10から5に減らしてみる。
ちょっと潰れてしまって見づらいので縦に伸ばしてみる。
これぐらいの幅に分布するものか、というのがわかるようになった。
これだけ単純化したモデルでも、5%のプレイヤーがデッキをランダムに選ぶだけで収束しなくなるゲーム、神ゲーか?
デッキタイプの多様性がなくなると、バランスが批判されがちだが、筆者としてはしっかりカードの枚数が変動するようにできているマジックというゲームは素直にすごいと思う。
特に私が大好きだった《荒野の再生》の時代なんかは・・・語りはじめると記事もう5本分ぐらいのボリュームになりそうなのでまた次の機会に語らせてもらいたい。
さて、ここまでの結果を見てわかることとして、除去がだいぶ多い。
勝つことよりも負けを回避することを優先し引き分けるデッキに収束してしまうようである。
実際に最終日に残ったデッキ2つを対戦させると、だいたい95%以上引き分けになってしまう。
少なくともこの設定では除去はクリーチャーに優越すると言えてしまいそうである。
とはいえ、設定が悪かったかもしれないので、変えてみる。ある配分の点で除去とクリーチャーの優先度が逆転するのを発見して参考にするという目的がある。しかし、引き分けるデッキは参考にすべきではない。条件を変えて、ライフを削って勝つ割合が多くてかつ、除去とクリーチャーの両方が採用されるようなものを見つけてみたい。
まずは単純にクリーチャーを強くして見る。
3/3/3を2/3/3へ。
とりあえず、ランダムなデッキを選ぶ個体はなし。
まだ除去が優先で引き分けるほうに収束してしまった。まあ2マナと1マナで交換できるならそうなってしまうか。土地1で詰まっても2で出てきたクリーチャーと交換を続けられるので、そういわれればそりゃあそう。
5%のランダムなデッキを選ぶ個体を入れて分布を見ると、クリーチャー数が上にいってはいるが。
クリーチャーをもうだいぶ強くして見る。
ご都合クソアドオリカ //Convenient Fuckin-Advantage Ori-Ca (2) クリーチャー
このクリーチャーはブロックされない。あなたのアップキープの開始時にあなたはカードを1枚引く。
3/3
ランダムにデッキを選ぶ個体なし。
やはり引き分ける方に収束してしまう。
除去で損せずに交換できる以上、クリーチャーのバリューを上げる方向だとそれを処理できる除去のバリューも上がってしまい、結果は同じようなものになりそうだ。
ではいっそ、除去した側が明確にアド損するように条件を変えてみる。次はこういうキャントリクリーチャーを使う。
②ご都合アド損しないオリカ //Convenient Never Disad Ori-Ca (3) クリーチャー
このクリーチャーは攻撃可能なら攻撃する。このクリーチャーはブロック可能ならブロックする。このクリーチャーが戦場に出たとき、カード1枚を引く。
3/3
マナコストが3なので、アド損するとはいえ、4マナからクリーチャー+除去と動いたり、除去でテンポを取る利もそれなりにありそうである。
まずはランダムなデッキを選ぶ個体なし
土地19、クリーチャー26、除去15に収束。
そう!こういうのが見たかった!
土地を伸ばしてクリーチャーを3Tに出やすくしたり、ダブルアクションを取りたいので、土地枚数もさっきまでの条件よりかなり増えている。
5%のランダムにデッキを選ぶ個体を入れた場合の広がりも見てみる。
一応一つ参考にできそうな結果が出たので、今回はこんなところでおしまい。
ChatGPTを触り始めたらはまってしまって、正直なところこの記事を書いてる途中にもっと面白い使い方をたくさん思いついてめんどくさくなってきてしまい書き上げるのが大変だった。また気が向いたら改めてしっかり調べてみたいテーマではあるが。
正直今回の試行ってChatGPTいらんくね?と言われてしまいそうだが、一応2時間ぐらいかかりそうなコーディングが1時間で終わって時短になってうれしかった、ChatGPTのどの部分が信頼できるかの理解が進んだという成果があったということでここはひとつ勘弁していただきたい。
もっと面白い使い方を見つけて近いうちに記事を書きたいと思うので、またよろしく!
また、相談やChatGPTについての知見共有はカードゲームのことはもちろんその他のことでも歓迎しますのでお気軽に私のTwitterまで。
この記事が参加している募集
おもろいこと書くやんけ、ちょっと金投げたるわというあなたの気持ちが最大の報酬 今日という日に彩りをくれてありがとう