Try   HackMD

サン・トウカできのみを貰う処理を例にします。処理は以下の通りです。

  • 乱数を1個取得する
  • 乱数を8で割った余りを計算する
  • 以下のリストと照合し、対応するインデックスのきのみが貰える。
    • [ クラボ, カゴ, モモン, チーゴ, ナナシ, ヒメリ, オレン, キー ]

これを、ライブラリをまったく使わずに書くと以下のようになります。

String PrettyPetal(uint seed) { var berries = new string[] { "クラボ", "カゴ", "モモン", "チーゴ", "ナナシ", "ヒメリ", "オレン", "キー" }; seed = seed * 0x41C64E6Du + 0x6073u; var rand = seed >> 16; var berryIndex = rand % 8; var berry = berries[berryIndex]; return berry; }

このような書き方はいくつか問題があります。すぐに思いつくだけでも以下のようなものがあります。

  • seedの更新処理に使われる定数がべた書きになっている
    • 定数を間違う恐れがある(筆者は何度も書いたので暗記しています)
    • 定数がマジックナンバーになっており、初見では読む人に意味が伝わらない
  • seedの更新と乱数取得の順序を間違う恐れがある
    • 古くに作られたツールでは「seedと乱数の対応関係を1つ分ずらす」という対応をしているものも多いが、これは本来存在しない『見かけ上の消費』を導入しなければならなくなる。「甘い香りで1消費」が有名な例。
  • 「乱数の取得」という意味のあるひと塊の処理がどこからどこまでなのかがわかりづらい

PokemonPRNGを使うと以下のように書けます。

using PokemonPRNG.LCG32.StandardLCG; String PrettyPetal(uint seed) { var berries = new string[] { "クラボ", "カゴ", "モモン", "チーゴ", "ナナシ", "ヒメリ", "オレン", "キー" }; var berryIndex = seed.GetRand(8); var berry = berries[berryIndex]; return berry; }

(ここでもう力尽きた)

主な機能

LCGの操作として頻出する以下の処理が提供されています。

  • seedを1つ進める : Advance()
  • seedを一度に複数進める : Advance(n)
  • seedを1つ戻す : Back()
  • seedを一度に複数戻す : Back(n)
  • 1つ進めたseedを得る(元の値は変更しない) : NextSeed()
  • 複数進めたseedを得る(元の値は変更しない) : NextSeed(n)
  • 1つ戻したseedを得る(元の値は変更しない) : PrevSeed()
  • 複数戻したseedを得る(元の値は変更しない) : PrevSeed(n)
  • 乱数を取得する(seedは1つ進む): GetRand()
  • 乱数をmで割った余りを取得する(seedは1つ進む): GetRand(m)
  • 0からいくつ進めたseedかを取得する : GetIndex()
  • initialSeedからいくつ進めたseedかを取得する : GetIndex(initialSeed)

Advance(n)GetIndex()は効率的なアルゴリズムで実装されているため、処理速度の低下を気にすることなく使うことができます。

乱数処理のコンポーネント化

乱数処理を再利用可能にするために、部品(コンポーネント)化しましょう。
「乱数を使った生成処理をコンポーネント化したもの」を表すインタフェース IGeneratable が用意されています。

class PrettyPetalBerryGenerator : IGeneratable<string> { private static readonly string[] _berries = { "クラボ", "カゴ", "モモン", "チーゴ", "ナナシ", "ヒメリ", "オレン", "キー" }; public string Generate(uint seed) => _berries[seed.GetRand(8)]; }

これを利用する側は次のように書きます。

var seed = 0xBEEFCAFE; var prettyPetal = new PrettyPetalBerryGenerator(); var berry = prettyPetal.Generate(seed);

また、AdvanceGetRand などに合わせて、seed を主語にして書けるように、レシーバと引数を逆転させる拡張メソッドも用意されているため、以下のようにも書けます。

var seed = 0xBEEFCAFE; var prettyPetal = new PrettyPetalBerryGenerator(); var berry = seed.Generate(prettyPetal);

ホウエン地方には他にもランダムにきのみをくれるNPCがいます。他のNPCのバリエーションも作成してみましょう。

class LilycoveCityBerryGenerator : IGeneratable<string> { private static readonly string[] _berries = { "クラボ", "カゴ", "モモン", "チーゴ", "ナナシ", "ヒメリ", "オレン", "キー", "ラム", "オボン" }; public string Generate(uint seed) => _berries[seed.GetRand(10)]; } class BerryMasterBerryGenerator : IGeneratable<string> { private static readonly string[] _berries = { "ザロク", "ネコブ", "タポル", "ロメ", "ウブ", "マトマ", "モコシ", "ゴス", "ラブタ", "ノメル" }; public string Generate(uint seed) => _berries[seed.GetRand(10)]; }

同じような処理が繰り返し現れています。重複を無くしましょう。

abstract class BerryGenerator : IGeneratable<string> { private static readonly string[] _berries; public BerryGenerator(string[] berries) => _berries = berries; public string Generate(uint seed) => _berries[seed.GetRand((uint)_berries.Length)]; } class LilycoveCityBerryGenerator : BerryGenerator { public LilycoveCityBerryGenerator() : base(new string[] { "クラボ", "カゴ", "モモン", "チーゴ", "ナナシ", "ヒメリ", "オレン", "キー", "ラム", "オボン" }); } class BerryMasterBerryGenerator : BerryGenerator { public BerryMasterBerryGenerator() : base(new string[] { "ザロク", "ネコブ", "タポル", "ロメ", "ウブ", "マトマ", "モコシ", "ゴス", "ラブタ", "ノメル" }); }

「ランダムに貰えるきのみの処理はほぼ定型だけど、きのみのラインナップを差し替えてバリエーションを作りたい」「きのみのラインナップは既知のデータとして提供したい」を両立するために継承を使っています。この程度なら穏当な継承の使い方と言えるでしょう(ただし継承を使わないと両立ができないわけではありません)。

他のインタフェース

IGeneratableのほかにIGeneratableEffectfulILcgUserILcgUtilizerが用意されています。

IGeneratableEffectful

IGeneratableは引数に受け取ったseedを変更しないのに対し、IGeneratableEffectfulは受け取ったseedを書き換えます。生成結果のリストを計算したいときはseedを1つずつ進めながら生成結果を列挙したいため、seedを書き換えないIGeneratableを使うと良いでしょう。一方でコンポーネント化された乱数処理を内部的に使用するような場合はseedも一緒に進める必要があります。そのような場合はIGeneratableEffectfulを使いましょう。
拡張メソッドはIGeneratableと同様にseed.Generate(IGeneratableEffectful)です。generatorのクラスが両方のインタフェースを実装している場合、seed.Generate(generator)IGeneratableIGeneratableEffectfulのどちらを対象とすれば確定できなくなりコンパイルが通らなくなるため、var generator = new HogeGenerator() as IGeneratableのように、型をどちらかのインタフェースとして指定しておきましょう。

ILcgUser

「乱数処理の生成結果には興味が無く、消費を発生させる用途で使いたい」ようなものにはILcgUserが使えます。こちらはseedを主語とする拡張メソッドはUsedという名前になっています。

ILcgUtilizer

ILcgUtilizerは…IGeneratableEffectfulと同じ定義になってますね…。何らかの結果を生成するのが主作用で「乱数の消費」を副作用と見なしたい場合はIGeneratableEffectfulを、「乱数の消費」が主作用で、副作用として得られる生成結果も利用したい場合はILcgUtilizerを使う…というような使い分けを想定していたような気がします。そのうちしれっと削除するかもしれません。。。

LINQとの連携

var results = seed.NextSeed(1000) .EnumerateSeed() .EnumerateGeneration(generator) .WithIndex(1000) .Take(9000) .Where((result) => result.Content.Nature == Nature.Adamant); foreach(var (frame, individual) in results) { Console.WriteLine($"{frame}F {individual.PID:X8} {individual.Nature}"); }

慣れると便利です。