私はWebゲーム開発者で、乱数に問題があります。プレイヤーが剣でクリティカルヒットを得る可能性が20%あるとします。つまり、5回のヒットのうち1回がクリティカルになるはずです。問題は、実生活で非常に悪い結果を得たことです。プレーヤーは5ヒットで3クリティカル、時には15ヒットで3クリティカルになります。戦闘はかなり短い(3〜10ヒット)ため、適切なランダム分布を取得することが重要です。
現在はPHP mt_Rand()
を使用していますが、コードをC++に移動しているだけなので、ゲームの新しいエンジンでこの問題を解決したいと思います。
解決策が一定のランダムジェネレーターであるかどうか、または適切な配布を強制するために以前のランダム状態を覚えているかどうかはわかりません。
一部のゲームの小さな実行での実際のランダム性は望ましくないという以前の回答に同意します-いくつかのユースケースでは不公平すぎるようです。
Rubyで実装のような単純なシャッフルバッグを作成し、いくつかのテストを行いました。実装はこれを行いました。
境界確率に基づいて不公平とみなされます。たとえば、20%の確率では、下限を10%に、上限を40%に設定できます。
これらの境界を使用して、10ヒットの実行で、14.2%の時間で、真の擬似ランダム実装がこれらの境界外の結果を生成しました。約11%の確率で、10回の試行で0のクリティカルヒットが記録されました。時間の3.3%で、10回のうち5回以上のクリティカルヒットが発生しました。当然、このアルゴリズム(最小ロールカウント5)を使用すると、かなり少ない量(0.03%)の "フェアリー"ランが範囲外になりました。 。以下の実装が不適切(確かにもっと賢いことができる)としても、ユーザーが実際の擬似ランダムソリューションとは不公平だと感じることがしばしばあることに注意する価値があります。
これが、Rubyで書かれた私のFairishBag
の中身です。実装全体と迅速なモンテカルロシミュレーション こちら(Gist)から入手可能 。
def fire!
hit = if @rolls >= @min_rolls && observed_probability > @unfair_high
false
elsif @rolls >= @min_rolls && observed_probability < @unfair_low
true
else
Rand <= @probability
end
@hits += 1 if hit
@rolls += 1
return hit
end
def observed_probability
@hits.to_f / @rolls
end
更新:この方法を使用すると、クリティカルヒットを取得する全体的な確率が、上記の境界を使用して約22%に増加します。これを相殺するには、「実際の」確率を少し低く設定します。フェアリッシュの修正で17.5%の確率では、観測された長期確率は約20%になり、短期の走行は公平に感じられます。
つまり、5回のヒットのうち1回がクリティカルになるはずです。問題は、実際の結果が非常に悪いということです-プレイヤーは5ヒットで3クリティカル、時には15ヒットでゼロになります。
必要なのは shuffle bag です。真のランダムがゲームにはランダムすぎるという問題を解決します。
アルゴリズムは次のようなものです。1つのクリティカルヒットと4つの非クリティカルヒットをバッグに入れます。次に、バッグ内の順序をランダム化し、一度に1つずつ取り出します。バッグが空になったら、同じ値を再度入力してランダム化します。これにより、5回のヒットにつき平均1回のクリティカルヒット、最大で2回のクリティカルヒットと8回の非クリティカルヒットが連続して取得されます。バッグ内のアイテムの数を増やして、ランダム性を高めます。
求めている動作を考えると、間違った変数をランダム化していると思います。
thisヒットがクリティカルになるかどうかをランダム化するのではなく、次のクリティカルヒットが発生するまでターン数をランダム化してみてください。たとえば、プレイヤーがクリティカルになるたびに2〜9の数字を選択し、そのラウンドが終了した後に次のクリティカルを与えるだけです。また、ダイス法を使用して正規分布に近づけることもできます。たとえば、2D4ターンで次のクリティカルを獲得できます。
この手法は、オーバーワールドでランダムに遭遇するRPGでも使用されると信じています-ステップカウンターをランダム化し、その多くのステップの後、再びヒットします。連続した2回のエンカウンターにヒットすることはほとんどないため、より公平に感じられます。それが一度でも発生すると、プレイヤーはイライラします。
まず、「適切な」分布を定義します。乱数は、まあ、ランダムです-あなたが見ている結果は、(疑似)ランダム性と完全に一致しています。
これを拡張して、私はあなたが望むものが「公平」の感覚であると思うので、ユーザーは成功せずに100ターン行くことはできません。その場合、最後の成功以降の失敗の数を追跡し、生成された結果に重みを付けます。 5ロールのうち1ロールを「成功」させたいと仮定しましょう。したがって、1から5までの数字をランダムに生成し、5であれば素晴らしいです。
そうでない場合は、障害を記録し、次回は1〜5の数値を生成しますが、たとえばfloor(numFailures/2)を追加します。今回も、5回に1回のチャンスがあります。失敗した場合、次回の勝ち間隔は4 and 5; 5分の2の成功率。これらの選択により、8回の失敗後、確実に成功します。
この記事があなたの役に立つことを願っています: http://web.archive.org/web/20090103063439/http://www.gamedev.net:80/reference/design/features/randomness/
「乱数」を生成するこの方法は、rpg/mmorpgゲームでは一般的です。
それが解決する問題はこれです(抽出):
ブレードスパイダーが喉にあります。当たり、見逃します。それは再びヒットし、あなたは再び逃します。そして、何度も何度も、あなたが何も打つことがなくなるまで。あなたは死んでいて、あなたの死体の上に輝く2トンのクモ形類がいます。不可能な?ありえない?はい。しかし、十分なプレーヤーと十分な時間を考えると、ありそうもないことはほぼ確実になります。ブレードスパイダーが硬かったのではなく、運が悪かっただけです。イライラする。プレーヤーをやめさせれば十分です。
必要なのは乱数ではなく、人間にとってはランダムに見える数字です。シャッフルバッドのような、個々のアルゴリズムを既に提案している人もいます。
このドメインの詳細で広範な分析については、 AI Game Programming Wisdom 2 を参照してください。本全体はどのゲーム開発者にとっても読む価値があります。「一見ランダムな数字」という考え方は、章で扱います。
AI決定およびゲームロジックのフィルタリングされたランダム性:
要約:従来の知恵では、乱数ジェネレーターが優れているほど、ゲームは予測不能になることが示唆されています。しかし、心理学の研究によると、短期間の真のランダム性は、しばしば人間にとって明らかにランダムではないように見えます。この記事では、強力な統計的ランダム性を維持しながら、ランダムなAIの決定とゲームロジックをプレーヤーにとってよりランダムに見えるようにする方法を示します。
また、別の章が興味深い場合があります。
乱数の統計
要約:乱数は、人工知能やゲーム全般で最も頻繁に使用されます。それらの可能性を無視することは、ゲームを予測可能かつ退屈にすることです。それらを誤って使用すると、それらを完全に無視するのと同じくらい悪いことがあります。乱数の生成方法、制限、機能を理解すると、ゲームで乱数を使用する際の多くの困難を取り除くことができます。この記事では、乱数、その生成、および良いものと悪いものを区別する方法についての洞察を提供します。
確かに、乱数を生成すると、そのような実行が発生する可能性がありますか?適切な割合を確認するのに十分な大きさのサンプルセットを3〜10ロールで取得することはできません。
たぶんあなたが望むのは慈悲の閾値です...最後の10回のロールを覚えておいてください、そしてそれらがクリティカルヒットを持っていなかったなら、それらに景品を与えてください。ランダムのスリングと矢印を滑らかにします。
最善の解決策は、複数の異なるnon randomスキームを使用したプレイテストを行い、プレイヤーを最も幸せにするスキームを選択することです。
たとえば、プレーヤーが最初のターンで1
を振って受け入れた場合など、特定のエンカウンターで同じ番号のバックオフポリシーを試すこともできます。別の1
を取得するには、2つの1
sを連続してロールする必要があります。 3番目の1
を取得するには、3行連続で、無限に必要です。
残念ながら、あなたが求めているのは事実上非乱数ジェネレータです-次の数を決定するときに以前の結果を考慮に入れたいからです。これは、乱数ジェネレーターの動作方法ではありません。
5ヒットごとに1ヒットをクリティカルにしたい場合は、1から5の間の数字を選んで、そのヒットがクリティカルになると言います。
mt_Rand()は、 Mersenne Twister 実装に基づいています。これは、取得可能な最良のランダム分布の1つが得られることを意味します。
どうやらあなたが望むのはランダムではないので、あなたが望むものを正確に指定することから始めるべきです。おそらく矛盾する期待があることに気付くでしょう-結果は真にランダムで予測不可能であると同時に、指定された確率からの局所的な変動を示すべきではありません-しかし、それは予測可能になります。最大10の非クリティカルを連続して設定した場合、「連続して9つの非クリティカルがあった場合、次のクリティカルは100%の確実性でクリティカルになる」とプレイヤーに伝えました。ランダム性をまったく気にしません。
以前に生成された数値を追跡するか、すべての可能な値をシャッフルすることを示唆する多くの回答があります。
個人的に、私は同意しません、3つの連続したクリットが悪いことです。また、15の非批判が連続して悪いことにも同意しません。
私は、各番号の後に、クリティカルチャンスを自己修正することによって、問題を解決します。例(アイデアを示すため):
int base_chance = 20;
int current_chance = base_chance;
int hit = generate_random_number(0, 100) + 1; // anything from 1 to 100
if(hit < current_chance)//Or whatever method you use to check
{
//crit!
if(current_chance > base_chance)
current_chance = base_chance; // reset the chance.
else
current_chance *= 0.8; // decrease the crit chance for the NEXT hit.
}
else
{
//no crit.
if(current_chance < base_chance)
current_chance = base_chance; // reset the chance.
else
current_chance *= 1.1; // increase the crit chance for the NEXT hit.
//raise the current_chance
}
クリティカルが発生しない時間が長くなるほど、次のアクションがクリティカルになる可能性が高くなります。私が含めたリセットは完全にオプションであり、それが必要かどうかを判断するにはテストが必要です。長い非クリティカルアクションチェーンの後に、連続した複数のアクションに対してクリティカルの可能性を高くすることが望ましい場合と望ましくない場合があります。
ちょうど2セントを投げる...
このような少数のテストでは、次のような結果が期待できます。
真のランダム性は、巨大なセットサイズでのみ予測可能です。そのため、コインをフリップし、最初に3回連続してヘッドを獲得することはかなり可能ですが、数百万回を超えると、およそ50〜50になります。
反復値を阻止する分布が必要な場合は、単純な反復拒否アルゴリズムを使用できます。
例えば.
int GetRand(int nSize)
{
return 1 + (::Rand() % nSize);
}
int GetDice()
{
static int nPrevious=-1;
while (1) {
int nValue = GetRand(6);
// only allow repeat 5% of the time
if (nValue==nPrevious && GetRand(100)<95)
continue;
nPrevious = nValue;
return nValue;
}
}
このコードは、繰り返し値を95%の時間拒否し、繰り返しはほとんどありませんが不可能ではありません。統計的には少しいですが、おそらくあなたが望む結果を生むでしょう。もちろん、「5 4 5 4 5」のような配布を妨げることはありません。あなたは手の込んだものにして、最後から2番目(たとえば)の60%を拒否し、最後から3番目(たとえば)の30%を拒否することができます。
私はこれを良いゲームデザインとして推奨していません。単にあなたが望むものを達成する方法を提案する。
上位のいくつかの答えは素晴らしい説明です。そのため、neverが決定論的になる一方で、「悪いストリーク」の確率を制御できるアルゴリズムに焦点を当てます。ここにあなたがすべきだと思うものがあります:
クリティカルヒットの確率であるベルヌーイ分布のパラメーターであるpを指定する代わりに、aおよびbのパラメーターを指定しますベータ分布、ベルヌーイ分布の「共役事前」。 [〜#〜] a [〜#〜]および[〜#〜] b [〜#〜]を追跡する必要があります。 -クリティカルヒット。
ここで、aおよびbを指定するには、クリティカルヒットの可能性であるa /(a + b)= pを確認します。すてきなことは、(a + b)が一般的にA /(A + B)をpにどれだけ近づけたいかを定量化することです。
次のようにサンプリングを行います。
p(x)
をベータ分布の確率密度関数とします。多くの場所で利用できますが、gsl_ran_beta_pdfとしてGSLで見つけることができます。
S = A+B+1
p_1 = p((A+1)/S)
p_2 = p(A/S)
確率p_1 /(p_1 + p_2)のベルヌーイ分布からサンプリングすることにより、クリティカルヒットを選択します。
乱数の「悪い縞」が多すぎる場合は、aとbをスケールアップしますが、aとb無限に進み、前述のシャッフルバッグアプローチを使用します。
これを実装する場合は、その方法を教えてください!
まあ、少し数学に興味があれば、おそらく 指数分布 を試すことができます。
たとえば、lambda = 0.5の場合、期待値は2です(その記事を読んでください!)。つまり、2ターンごとにヒット/クリティカル/何でも(50%など)ヒットする可能性が最も高いことを意味します。しかし、このような確率分布を使用すると、0ターン(イベントが既に発生し、turn_counterがリセットされたターン)で、完全にミス(または何でも反対)を定義し、次のターンにヒットする可能性が40%、約65% 2番目(次の次)のターン、約80%で3番目にヒットするなどのチャンスがあります。
その分布の全体的な目的は、ヒット率が50%で、3回連続でミスした場合、確実にヒットします(80%以上の確率で、次のターンごとに増加します)。これにより、より多くの「公平な」結果が得られ、50%以上のチャンスが変わらずに維持されます。
クリティカルの20%のチャンスをとると、
5回の結果のターンで、まだ約0.2%(これらの5%に対して)の確率は3クリット+ 2ノンクリットです。そして、結果として4つの非クリティカルの14%、5の5%、6の1.5%、7の0.3%、8の非クリティカルの0.07%です。私は41%、32%、26%、21%、16%よりも「公正」だと思います。
まだ退屈しないでください。
あなたが望むものは本当に明確ではありません。最初の5回の呼び出しで、ランダムな順序で1〜5の数値を返すような関数を作成することができます。
しかし、それは本当にランダムではありません。プレーヤーは、次の5つの攻撃で正確に1つの5を獲得することを知っています。それはあなたが望むものかもしれませんが、その場合は、自分でコーディングするだけです。 (数字を含む配列を作成し、シャッフルします)
または、現在のアプローチを使用し続け、現在の結果が不良なランダムジェネレーターによるものであると想定することもできます。現在の番号に問題はないことに注意してください。ランダム値はランダムです。時々、同じ値の2、3、または8が連続して得られます。ランダムだからです。優れたランダムジェネレーターは、平均して、すべての数値が等しく頻繁に返されることを保証するだけです。
もちろん、不正なランダムジェネレーターを使用している場合、結果が歪んでいる可能性があります。その場合は、単により良いランダムジェネレーターに切り替えるだけで問題が解決します。 (より良いジェネレーターについては、Boost.Randomライブラリーをご覧ください)
あるいは、ランダム関数によって返された最後のN個の値を覚えて、それらの結果を比較検討することもできます。 (簡単な例は、「新しい結果が発生するたびに、値を破棄して新しい値を取得する必要がある確率は50%です」
推測しなければならない場合、「実際の」ランダム性に固執するのが最善の策だと思います。優れたランダムジェネレーターを使用していることを確認してから、今のやり方を続けてください。
Blizzardが使用するようなプログレッシブパーセンテージシステムをお勧めします。 http://www.shacknews.com/onearticle.x/57886
通常、RNGをロールしてから値と比較して、成功するかどうかを判断します。次のようになります。
if ( randNumber <= .2 ) {
//Critical
} else {
//Normal
}
あなたがする必要があるのは、ベースチャンスの漸進的な増加を追加することです...
if (randNumber <= .2 + progressiveChance ) {
progressiveChance = 0;
//Critical
} else {
progressiveChance += CHANCE_MODIFIER;
//Normal hit
}
もっと凝ったものにする必要がある場合は、追加するのはとても簡単です。プログレッシブチャンスが100%のクリティカルチャンスを回避するために獲得できる量を制限するか、特定のイベントでリセットすることができます。また、プログレッシブチャンス+ =(1-プログレッシブチャンス)* SCALE <SCALE <1のように、各ブーストでプログレッシブチャンスを少しずつ増加させることもできます。
1から5までの数字を含むリストを作成し、それらをランダムに並べ替えることができます。次に、作成したリストを確認します。少なくとも1回はすべての数字に出会うことが保証されています...最初の5つで問題がなければ、さらに5つの数字を作成します...
クリティカルになる可能性については、最後のN個の攻撃に依存します。 1つの単純なスキームは、ある種のマルコフ連鎖です。 http://en.wikipedia.org/wiki/Markov_chain しかし、コードはとにかく非常に単純です。
IF turns_since_last_critical < M THEN
critial = false
turns_since_last_critical++;
ELSE
critial = IsCritical(chance);
IF Critial THEN
turns_since_last_critica = 0;
ELSE
turns_since_last_critica++;
END IF;
END IF;
もちろん、最後のターン以降に十分なターンになったことがわかったら、クリティカルの可能性はクリティカルの可能性よりも低いため、数学を作成する必要があります。
OP、
あなたがそれを公正にしたいなら、それはランダムではありません。
ゲームの問題は、実際の試合の長さです。一致が長ければ長いほど、表示されるランダム性が少なくなり(クリティカルは20%になる傾向があります)、意図した値に近づきます。
以前のロールに基づいて攻撃を事前計算するという2つのオプションがありました。 5回の攻撃(20%に基づく)ごとに1つのクリティカルを取得しますが、ランダムに発生する順序にすることができます。
listOfFollowingAttacks = {Hit、Hit、Hit、Miss、Crit};
それがあなたが望むパターンです。したがって、そのリストからランダムに選択し、空になるまで再作成します。
それは自分のゲーム用に作成したパターンで、非常にうまく機能します。
2番目のオプションは、クリティカルの機会を増やすことです。おそらく、すべての攻撃の最後に、より偶数の数字が表示されます(試合がかなり早く終了すると仮定します)。確率が低いほど、RNGが多くなります。
これは本当に予測可能です...しかし、あなたは決して確信できません。
City of Heroesには、実際にこの問題を正確に解決する「ストリークブレイカー」と呼ばれるメカニズムがあります。動作方法は、文字列内の最低ヒット確率に関連する長さの一連のミスの後、次の攻撃がヒットすることが保証されることです。たとえば、ヒット率が90%を超える攻撃を逃した場合、次の攻撃は自動的にヒットしますが、ヒット率が60%のように低い場合、「ストリークブレイカー」をトリガーするには連続して数回ミスする必要があります(I正確な数がわからない)
おそらく正規分布が必要なときに、線形分布を見ています。
青春時代にD&Dをプレイしたことを思い出すと、複数のn面ダイスを振って結果を合計するように求められました。
たとえば、4 x 6面のサイコロを振るのは、1 x 24面のサイコロを振るのとは異なります。
ランダムクリティカルヒットを事前に計算しますfor each player。
// OBJECT
//...
// OnAttack()
//...
c_h = c_h -1;
if ( c_h == 0 ) {
// Yes, critical hit!
c_h = random(5) + 1 // for the next time
// ...
}
私もこの問題を解決しようとしました。私が思いついたのは、過去にどれだけの頻度で出現したかに基づいて確率を動的に変更することでした。次のようなもの(サイコロ用、MATLAB):
probabilities = [1 1 1 1 1 1];
unrandomness = 1;
while true
cumprob = cumsum(probabilities) ./ sum(probabilities);
roll = find(cumprob >= Rand, 1)
probabilities = probabilities + unrandomness;
probabilities(roll) = probabilities(roll) - 6*unrandomness;
if min(probabilities) < 0
probabilities = probabilities - min(probabilities);
end
end
インデントが不足してすみません。ランダム性パラメータは、必要に応じて調整できます。真のランダム出力(unrandomness = 0):
2 3 1 1 4 1 3 3 4 5 1 5 5 2 6 1 1 1 6 1 1 3 5 6 6 1 4 2 4 6 3 6 5 1 1 6 2 5 6 4 3 5 2 4 6 5 5 5 4 4 3 4 1 2
unrandomness = 1:
3 2 4 5 6 2 6 2 4 1 5 5 1 6 4 3 1 4 2 1 3 2 6 5 3 6 5 3 1 4 1 6 5 3 4 2 1 6 5 4 1 4 3 1 6 6 5 4 3 1 5 2 3 2
良く見えます。特に、数値間のギャップをプロットする場合。
反応:「問題は、実際の結果が非常に悪いことです。5ヒットで3クリティカル、15ヒットで3クリティカルになることもあります。」
15ヒットで3〜4%の確率で何も得られない可能性があります...
値の重み付けはどうですか?
たとえば、クリティカルヒットの可能性が20%の場合、クリティカルヒットを表す1つの数字で1〜5の数字、またはクリティカルヒットである1〜100の数字を生成します。
ただし、乱数または擬似乱数を使用している限り、現在表示されている結果を潜在的に回避する方法はありません。それがランダム性の性質です。
おそらくあなたは間違ったランダム分布関数を使用していると思います。おそらく、数字を均等に分配したくないでしょう。代わりに正規分布を試して、クリティカルヒットが「通常の」ヒットよりも少なくなるようにしてください。
私はJavaで作業しているので、正規分布で乱数を与えるC++の何かをどこで見つけることができるかわかりませんが、そこに何かがなければなりません。
ポアソン分布から乱数を生成する必要があると思います。
それはISどうあるべきか、それは確率だ、あなたは5バトルごとに1ヒットのクリティカルヒットを期待するべきではない。 。GB
次の「ランダムに遅延したプットバックダイ」を提案します。
in-array
)は最初に0からn-1までの値で埋められ、もう1つ(out-array
)は空ですin-array
の値in-array
からout-array
に移動しますout-array
からin-array
に1つのランダムな要素(未定義を含むall要素上に移動!)これには、より大きなnほど「反応」が遅くなるという特性があります。たとえば、20%のチャンスが必要な場合、nを5に設定して0を押すと、nを10に設定して0を押すか、または1、1000のうち0から199にすることは、小さなサンプルの真のランダム性とほとんど区別できません。サンプルサイズに合わせてnを調整する必要があります。
多くの人が言うように、それは本当に「ランダム」なものの問題です。あなたが得ている結果はランダムであり、ゲームをどのように作成しても、一部のプレイヤーはあなたのカウンターが公平ではなく、ランダムではないと感じるでしょう。 ;)
1つの可能なオプションは、n回ごとにヒットを保証し、各ヒット後に特定の境界内でnをランダムに生成することです。それはすべて、プレイテストで何を「感じる」のかという問題です。