ガラパゴスへよおこそ。
[PR]上記の広告は3ヶ月以上新規記事投稿のないブログに表示されています。新しい記事を書く事で広告が消えます。
これの使い方
UIを作る能力は私にはないので、c言語の知識を前提に、コードを直接いじってもらうことになります。
例1、青銅の鎧を先攻3ターン目に使用できる確率(ドローカード不使用)
【カードセット】
4x 青銅の鎧
15x 緑マナ
先攻3ターン目までに引く総手札枚数は7枚。この中に青銅の鎧と緑マナが各1枚以上あればいいわけです。
注意すべきは「緑マナ」の中に《青銅の鎧》が含まれていること。つまり《青銅の鎧》は《青銅の鎧》と「緑マナ」の両方として扱うべきカードということになります。
というわけで、カードセットは次のように訂正します。
【カードセット】
4x 青銅の鎧かつ緑マナ
11x 緑マナ
さて、調整すべき値は次の通り。
sizeof_hand
cardset[]
cardtype[]
sizeof_cardset
conditon
exception
trynum
ここではexception(例外処理)、cardtype[]は使いません。それぞれ0、{0,0,0}としておいてください。
上から順に見ていきましょう。
sizeof_handは手札枚数。ここは7枚。
cardset[]はカードの種類ごとの枚数を格納する配列。
cardset[0] = 4, cardset[1] = 11, cardset[2] = 40としておけばOKです。ここで[0]=青銅の鎧かつ緑マナ、[1]=緑マナ、[2]=全体という割り当てをしたことに注意。
sizeof_cardsetはカードの種類。ここでは2種類ですが、それ以外の25枚のカードの場所も必要なので、それを加えてsizeof_cardset = 3。
conditon(条件)は「何を以って成功とするか」。この場合は《青銅の鎧》とそれに必要な緑マナを引き当てることですから、
□「青銅の鎧かつ緑マナ」が1枚以上 かつ 「緑マナ」が1枚以上
□「青銅の鎧かつ緑マナ」が2枚以上
のいずれかになります。
「青銅の鎧かつ緑マナ」「緑マナ」を何枚引いたかはgroupcount[sizeof_cardset]に保存されています。先ほど割り当てたように、[0]=青銅の鎧かつ緑マナ、[1]=緑マナです。
これを使って条件をc言語化すると、上の2条件はそれぞれ
□groupcount[0]>0 && groupcount[1]>0
□groupcount[0]>1
となり、これを組み合わせた
(groupcount[0]>0 && groupcount[1]>0) || groupcount[0]>1
を最上部のdefine文に叩き込めばおkです。
最後にtrynum(試行回数)。少なくとも1000回程度は欲しいところ。多ければ多いほど精度が高まりますが、データ型がintなのでオーバーフローに注意。
例2、無頼勇騎ウインドアックスを後攻5ターン目に使用できる確率(ドローカード不使用)
ここでもexception(例外処理)、cardtype[]は使いません。それぞれ0、{0,0,0,0}としておいてください。
【カードセット】
4x 無頼勇騎ウインドアックス
16x 緑マナ
12x 赤マナ
まずカードセットを書き直して
【カードセット】
4x 無頼勇騎ウインドアックスかつ緑マナかつ赤マナ
12x 緑マナ
8x 赤マナ
次に
sizeof_hand = 10
cardset[0] = 4
cardset[1] = 12
cardset[2] = 8
cardset[3] = 40
sizeof_cardset = 4
を入力。
conditionはやや複雑ですが
□groupcount[0]>2
□groupcount[0]>1 && groupcount[1]>0
□groupcount[0]>1 && groupcount[2]>0
□groupcount[0]>0 && groupcount[1]>0 && groupcount[2]>0
これを||で繋いで完了。
trynumは適当に。条件やカードの種類が多くなると処理が遅くなるので、上手く調整してください。
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
//hit条件
#define hit_condition groupcount[0]+groupcount[3]+groupcount[4]+groupcount[6]>0 && groupcount[1]+groupcount[3]+groupcount[5]+groupcount[6]>0 && groupcount[2]+groupcount[4]+groupcount[5]+groupcount[6]>0
#define exception 0 //例外条件
#define sizeof_cardset 8 //cardset[]の要素数
#define sizeof_hand 8 //手札枚数
int main(void){
int trynum = 30000; //試行回数
int cardset[sizeof_cardset] = {4,6,12,2,1,2,0,40}; //扱うカードの、種類ごとの枚数
int cardtype[sizeof_cardset] = {0,0,0,1,1,1,1,0}; //カードの属性。現状レインボー(1)or単色(0)のみ
int groupcount[sizeof_cardset]; //グループカウンタ。要素数をcardset[]に追従して、手札中に種類Aのカードが1枚あれば同じ位置の要素をインクリメント
int hand[sizeof_hand]; //手札[手札枚数]
int i,k,j,l,m,n,p,q; //forループ用変数
int duplcount,siev,groupnum,try; //重複判定用カウンタ、ふるい分け用変数、手札種類保存用変数、試行番号
int hitcount = 0; //条件を満たした試行の回数を数えるカウンタ
int shuffle = 0; //乱数調整用変数
double probability; //確率
//「それ以外」の枚数設定
for(m=0; m<sizeof_cardset-1; m++){
cardset[sizeof_cardset-1] = cardset[sizeof_cardset-1]-cardset[m];
}
for(try=0; try<trynum; try++){
//グループカウンタのリセット
for(l=0; l<sizeof_hand; l++){
groupcount[l] = 0;
}
//手札生成
//過去のカードとの被り番号をチェックするのがforループ[k]
//被らないような番号になるまで乱数を作り直すのがdo-whileループ
//以上の操作を手札枚数分繰り返すのがforループ[i]
for(i=0; i<sizeof_hand; i++){
do{
duplcount = 0;
srand(try + i + shuffle + (unsigned int)time(NULL));
hand[i] = rand()%40;
for(k=0; k<i; k++){
if(hand[k] == hand[i]) duplcount++;
}
shuffle++;
}while(duplcount > 0);
}
//ふるい分け
//「カードに割り振った番号」を「カードの種類」に変換する
//変数sievに、cardset[]の値を番号の若いほうから加算していく
//siev>hand[n]となったときのjをgroupnumに保存しておき、ループ脱出後にこの位置の要素をインクリメント+hand[n]の値をgroupnumに置き換える
for(n=0; n<sizeof_hand; n++){
siev = 0;
for(j=0; j<sizeof_cardset; j++){
siev = siev + cardset[j];
groupnum = j;
if(siev > hand[n]) break;
}
hand[n] = groupnum;
if(exception){
hand[n] = sizeof_cardset-1;
}else{
groupcount[groupnum]++;
}
}
//条件判定
//hithit_conditionはヘッダで定義する
if(hit_condition) hitcount++;
//動作確認用
printf("%d and %d\n", hitcount, try);
for(p=0; p<sizeof_cardset-1; p++){
printf("%d,", groupcount[p]);
}
printf("%d\n", groupcount[sizeof_cardset-1]);
for(q=0; q<sizeof_hand-1; q++){
printf("%d,", hand[q]);
}
printf("%d\n", hand[sizeof_hand-1]);
}
//確率計算
probability = 100.0*hitcount/trynum;
printf("P = %f\n", probability);
return 0;
}
解説書いた。汎用性もうpした。
必要に応じて"conditon","exception","sizeof_hand",''cardset[]","cardtype[]","sizeof_cardset","trynum"を調整してください。
for(int i = 0; i<N; i++)とかはやってないのでC90でもいけます。
計算速度は飛躍的にうp。一瞬で計算できます。
ただ乱数に偏りがあるっぽいのでそこが課題。具体的に言うと、秒が変わらない限り計算結果も同じになる。
頻繁に書き換えます。
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define N 8
int main(void){
int cardset[8] = {1,6,12,2,1,4,0,40};
int categorycount[8] = {0,0,0,0,0,0,0,0};
int hand[N];
int i,k,j,l,m;
int duplcount,siev,handnum,categorynum,try,trynum = 1000;
int hitcount = 0;
double probability;
//「それ以外」の枚数設定;cardset[7]==40
for(m=0; m<8; m++){
cardset[7] = cardset[7]-cardset[m];
}
for(try=0; try<trynum; try++){
//カテゴリカウンタのリセット
for(l=0; l<N; l++){
categorycount[l] = 0;
}
//手札生成
for(i=0; i<N; i++){
do{
duplcount = 0;
srand(try + i + (unsigned int)time(NULL));
hand[i] = rand()%40;
for(k=0; k<i; k++){
if(hand[k] == hand[i]) duplcount++;
}
}while(duplcount > 0);
}
//ふるい分け
for(handnum=0; handnum<N; handnum++){
siev = 0;
for(j=0; j<8; j++){
siev = siev + cardset[j];
categorynum = j;
if(siev > hand[handnum]) break;
}
categorycount[categorynum]++;
}
//条件判定
if(categorycount[0]+categorycount[3]+categorycount[4]+categorycount[6]>0 && categorycount[1]+categorycount[3]+categorycount[5]+categorycount[6]>0 && categorycount[2]+categorycount[4]+categorycount[5]+categorycount[6]>0) hitcount++;
//動作確認用
printf("%d and %d\n", hitcount, try);
printf("%d,%d,%d,%d,%d,%d,%d\n",categorycount[0],categorycount[1],categorycount[2],categorycount[3],categorycount[4],categorycount[5],categorycount[6],categorycount[7]);
printf("%d,%d,%d,%d,%d,%d,%d\n",hand[0],hand[1],hand[2],hand[3],hand[4],hand[5],hand[6],hand[7]);
}
//確率計算
probability = 100.0*hitcount/trynum;
printf("P = %f\n", probability);
return 0;
}
trynum=1000で計算したら8分もかかった(Athlon ll X4 630)
パターンAが成立する確率はいくらか、というのを求めるのには十分な速さ。
でも「100回回したときの「パターンAの成立確率」は真の確率とどのくらいズレがあるか」を統計学的に云々、ってのをやるには時間がかかりすぎるか。10回回したときの(ryを1000回やるとしても10000回=80分かかる計算だしな。
重複を避けて乱数生成する部分はまだ改善の余地ありそう
解説書くの面倒だったけど、個別の質問なら受けます
追記
1--61.4%
2--57.6%
3--59.1%
4--59.0%
「シナジー」という言葉はあまり良くないと最近思うようになった。なぜか。理由は2つ。
1、「時間」の概念が欠けている
"synergy"は日本語で「相乗効果」。要素とその関係が問題になるが、それだけ。動的要素や時間的順序が見落とされている。簡略化のために静的要素に絞るなら「シナジー」でいいんだけど、ことカードゲームに限って言えば、それはダメだ、というのが「シナジー」についての記事を書いてきた私の見解。
2、「外力の影響」を意識していない
外力とは「外から加わる力」。要するに相手プレイヤーの干渉。カードA、カードBがあります。相性良いです。これだけだ。相手プレイヤー、という登場人物が欠落している。(*1)
(*1)
メタを張る意識が欠けているという意味ではない。相手の妨害を前提にしていない、という意味。
ではどんな言葉が妥当か。私は「流れ」を推したい。
時間概念は当然出てくるし、流れをせき止めるものとしての相手プレイヤーの存在も、その動的なイメージから想起されると思うんだけど、どうだろう。
とまれ、ここから「シナジー」は「流れ」という言葉に置き換えることとしよう。
さて、「流れ」とは何か再考しよう。まず相乗効果。2枚以上のカードを使い、単体使用時よりも大きな成果を収めること。流れには上流と下流があり、ここでは上流→下流のように流れを表現することにする。上流→下流の順序を入れ替えて上流(元下流)→下流(元上流)のようにできるものもある。
「流れ」はせき止めることもできる。簡単にせき止められる水路もあればそうでないのもある。また、同じ種類の水路でも、上流から下流までの水路の長さ、すなわち時間的間隔が長いほど、せき止めやすくなる。
ここまでが「流れ」という言葉に持たせたかったニュアンスだ。俯瞰的な書き方をしてきたが、今度はより詳しく見ていこう。時間的順序はいったん飛ばして、水路の種類からはじめることにする。
■水路の種類
「流れ」において「下流」が「上流」を参照しなければならない。ここで「どこを」参照するかによって理論上6通りのパターンを考えることができる。
1、バトルゾーン
クリーチャーの進化、ミストリエスの使用、などなど(*2)。使用後バトルゾーンに残らない呪文や城を除けば、ほぼすべてのクリーチャーやクロスギアがここで「流れ」の「上流」になる可能性がある。ほかの例としては「邪魂創世」。これはバトルゾーンに小型クリーチャーやcip、pigが残るカードを「上流」に必要とする。
(*2)
後述するが、ハッスルキャッスルは「シールド」の区分である。
2、手札
「手札を何枚捨てる」のような、手札を参照するタイプの「下流」において、捨てられたカードは「上流」として扱われることがある。具体的には、捨てられることで何らかのメリットがある(バイケン)とか、必要のないカードを捨てることでデメリットを殺すとか。
バトルゾーンと違ってカードを使用する必要がないが、「下流」を使うまで手札に待機していなければならず、その間の選択肢量が下がってしまう問題がある。
3、マナ
チャージャー→マナ回収や、手札からマナチャージ→母なる星域など。物によるが、上流では必ずしもカードを使用する必要がない。手札からノーデメリットでマナチャージできるのが重要。除去される可能性は低めだが、ランデスの存在はマイナーではないし、龍神メタルのように高い性能のついでにランデス成分が入っているカードも若干あるのが気がかり。
4、墓地
ダンディナスオ→インフェルノサインなど。手札やマナほど上流が簡単ではなく、カードを墓地に送るには「相手に除去してもらう」か「自分から墓地に送るようなカードを使う」しかない。しかしいったん墓地に送ってしまえばそこから流れが止められることはまずない。
5、山札
テンペストベビー→転生プログラムや、バルガゲイザーを使った流れがここ。やはり「コーライルなどで相手に戻してもらう」か「自分から山札に戻すようなカードを使う」しかない。いったん山札に戻せば相手の直接的妨害はないものの、毎ターン必ずカードを1枚引かなければならないので、他の領域のように「相手の妨害がなければ下流を引き当てるまで待機していればよい」というわけにはいかない、というのが山札に特有な事情である。
6、シールド
スーパーエメラル→ベンゾなど。山札の場合と同様に「自分からシールドに送るようなカード」を使うしかないが、かなり種類は限られる。直接的に妨害するようなカードはこれまた少ないが、クリーチャーの攻撃によって妨害できることを考えれば、せき止めの危険はかなり大きい。現状では、1ターン中に連続して上流→下流と使ってしまうのが主流か。
簡約版がこっち。
水路の種類 | 上流の準備の容易さ | 安全性 | 備考 |
バトルゾーン | 中 | 低 | |
手札 | 高 | 低 | 上流カードの待機による手札選択肢量減 |
マナ | 高 | 中 | |
墓地 | 低 | 高 | |
山札 | 低 | 高 | 上流の待機時間に制約がある |
シールド | 低 | 中 |
基準は次の通り。
□上流の準備の容易さ
カードを引くだけで良い手札、マナが高。さらにコストがかかるバトルゾーンが中。さらにそれを移動させるための別カードが必要な墓地、山札、シールドは低。例外もあるけどだいたいあってると思う。
□安全性(相手の妨害に対しての安全性)
ほとんどアンタッチャブルな墓地、山札は高。クリーチャーに攻撃される危険とシールドブレイクによる手札増加メリットがジレンマとなるシールド、環境によるとしか言いようないマナは中。あらゆるデッキが干渉カードを積みたがるバトルゾーン、手札は低。
■時間的順序
これら6種類の水路を使った流れの、上流と下流を入れ替えても成立するのはどのようなものだろうか?
下流によって生じる効果(上流による相乗効果込)のうち、相手のカードへの干渉を除けば、自分のバトルゾーンへの還元が最も大きいと考えられる(コンボを用いてまでバトルゾーン以外の自分の領域に効果を還元する例(スナフマッシュルーム→邪魂創世とか)は比較的稀)。つまり下流にはバトルゾーンへ効果を向けたカードが多い。よって下流にはバトルゾーンを水路とするカードが多い。上流下流を入れ替えたときに流れが成立するなら、その水路はバトルゾーンである可能制が高い。だから最有力はバトルゾーンと言える。これ以外は、今のところ少ないか役に立たない。
この記事で言うところの「一方通行シナジー」「相互依存シナジー」がまさに時間的順序にあたる(前者は上流と下流を入れ替えると成立しないシナジー、後者は上流と下流を入れ替えても成立するシナジー)のだが、先ほどの話から、「相互依存シナジー」はA→BもB→Aもバトルゾーンを水路とすることがわかる。
時間的順序について言うべきことはこのくらい。最後に水路の長さの話を。
■水路の長さ
「水路の長さ」は別に難しい話ではない。バトルゾーンを水路とする流れの場合、上流の使用→下流の使用までを1ターン間隔(水路が短い)で行うか、2ターン間隔(水路が長い)で行うかでは、後者のほうが妨害される確率が高くなるというだけのことだ。ただしこれにも「水路の種類」が密接に関係していて、例えば墓地を水路とする場合、相手がコンクリオン(!)を積んでいないでない限り、いくら時間を空けても妨害される確率は全く変わらない。山札にも同じことが言える。シールド、マナはきわどい。
つまり、「水路の長さ」が流れの成功率に関わってくるのは、「水路の種類」がバトルゾーン、手札のときだけだという結論になる。
時間的順序、水路の長さの件も含めて「水路の種類」とその特徴をまとめておこう。
水路の種類 | 上流の準備の容易さ | 安全性 | 水路が長くなると安全性が | 備考 |
バトルゾーン | 中 | 低 | 下がる | 双方向の流れがあるものが比較的多い |
手札 | 高 | 低 | 下がる | 上流カードの待機による手札選択肢量減 |
マナ | 高 | 中 | やや下がる | |
墓地 | 低 | 高 | 変わらない | |
山札 | 低 | 高 | 変わらない | 上流の待機時間に制約がある |
シールド | 低 | 中 | やや下がる |
途中で参照したここの記事も、バトルゾーン水路という前提でマナブースト型とコントロール型を比較したものだと考えれば、すっきりして見えそうだ。バトルゾーン水路でなくても、同じ「水路の種類」という前提であればバトルゾーン水路の場合とほとんど同じことが言えるが、「相互依存シナジー」が抜けて「一方通行シナジー」と「シナジーなし」の2パターンの話になる点が異なる。