はじめに
この記事はPokémon RNG Advent Calendar 2017 10日目の記事です.
adventar.org
乱数調整で楽しむ方々の間では乱数調整を支援するツールのことを乱数ツールと呼んでいます. 僕も乱数ツールを作成する内の1人であり, 時々ツールを作成しますがツールのソースコードはどうしても複雑になりがちです. たかが計算ツールといえども綺麗に書きたいですよね.
この記事ではScalaを使い, 乱数ツールを綺麗に書いてみる話をします.
…あれ?🤔
Adventarのコメント欄にはデザインパターンの話をするって書いてあったはずなのにScalaの話?🤔
デザインパターンは???🤔🤔🤔
…申し訳ありませんが今回はテーマを変えて記事を書いています🙇 本当はデザインパターンの話を書こうと思っていたのですが記事に出来るほど知見が溜まっていなかったのでボツ 🚮 となりました. デザインパターンについてはまたいつか別の機会に話そうと思います 🙏
…さて話を戻します. なぜScalaを使って乱数ツールを書くかというと, Scalaには非常に充実したコレクションライブラリが備わっており, これを用いることで乱数列に対する複雑な操作を簡単に記述できるからです*1. 乱数ツールを記述していく中で, このコレクションライブラリがいかに力を発揮していくかを感じでもらえればと思います.
前提
- 次の条件を満たす読者を対象としています
- 3世代(RS/FRLG/Em) の野生乱数を扱います
- メソッドずれは発生しないものとします
Iteratorを継承したLCGを作成する
まずは乱数生成器(LCG)を作成しましょう. LCGクラスにIteratorトレイトを継承させることで, 乱数列をコレクションとして扱うことができます. そうすることで, コレクションライブラリで提供される非常に多くの便利なメソッドがLCGクラスで利用できるようになります.
class LCG(seed: Int, a: Int, b: Int) extends Iterator[Int] {
var state: Int = seed
override def hasNext: Boolean = true
override def next(): Int = {
state = state * a + b
state
}
def next(n: Int): Int = java.lang.Integer.remainderUnsigned(this.next(), n)
}
object Wandbox {
def main(args: Array[String]): Unit = {
val lcg = new LCG(0x00000000, 0x41c64e6d, 0x6073)
println(lcg.take(5).map(_.toHexString).toList)
}
}
サンプルコードの例ではコレクションのメソッド take()
, map()
, toList()
を使用して先頭5つの乱数を16進数表記で出力しています.
調律された乱数列を取得する
通常ではLCGで得られる生の乱数列は乱数性に問題があるため, 下半分のbitを切り落として利用されます. これを先程作成したLCGクラスを用いて書くと以下のようになります.
object Wandbox {
def temper(state: Int): Int = state >>> 16
def main(args: Array[String]): Unit = {
val lcg = new LCG(0x00000000, 0x41c64e6d, 0x6073)
val temperedLcg = (new LCG(0x00000000, 0x41c64e6d, 0x6073)).map(temper(_))
println(lcg.take(5).map(_.toHexString).toList)
println(temperedLcg.take(5).map(_.toHexString).toList)
}
}
写像を表わす map()
メソッドを用いて生の乱数を調律後の値へと変換しています. たったこれだけで, 生の乱数列を調律された乱数列に変換できます.
ただし, これでは map()
によって返ってくる型が Iterator[Int]
となってしまい, LCGクラスで実装したメソッド (def next(n: Int): Int
など) を呼び出せなくなってしまいます. これは後々困ったことになるのでここではLCGクラスを継承して調律された乱数列を生成するTemperedLCGクラスを作成することにしましょう.
class TemperedLCG(seed: Int, a: Int, b: Int) extends LCG(seed, a, b) {
override def next(): Int = super.next() >>> 16
}
object Wandbox {
def main(args: Array[String]): Unit = {
val lcg = new LCG(0x00000000, 0x41c64e6d, 0x6073)
val temperedLcg = new TemperedLCG(0x00000000, 0x41c64e6d, 0x6073)
println(lcg.take(5).map(_.toHexString).toList)
println(temperedLcg.take(5).map(_.toHexString).toList)
}
}
fork()
メソッドを実装する
LCGインスタンスはmutableな操作をするので, そのまま複数のスレッドに渡すと破滅します. 次は乱数列上の5つの乱数を出力する処理を, 1消費ずつずらしながら各々のスレッドで実行する例です*2.
object Wandbox {
def printRands(lcg: LCG, n: Int) = println(for (i <- 1 to n) yield lcg.next())
def main(args: Array[String]): Unit = {
val lcg = new LCG(0x00000000, 1, 1)
(1 to 3).map(_ => {
val f = Future {
printRands(lcg, 5)
}
lcg.drop(1)
f
}).foreach(Await.ready(_, Duration.Inf))
}
}
そこで自身のクローンを作成する fork()
メソッドを実装してクローンをスレッドに渡すようにしてみましょう.
import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
class LCG(seed: Int, a: Int, b: Int) extends Iterator[Int] {
def fork(): LCG = new LCG(state, a, b)
}
class TemperedLCG(seed: Int, a: Int, b: Int) extends LCG(seed, a, b) {
override def fork(): TemperedLCG = new TemperedLCG(state, a, b)
}
object Wandbox {
def printRands(lcg: LCG, n: Int) = println(for (i <- 1 to n) yield lcg.next())
def main(args: Array[String]): Unit = {
val lcg = new LCG(0x00000000, 1, 1)
(1 to 3).map(_ => {
val forkedLcg = lcg.fork()
val f = Future {
printRands(forkedLcg, 5)
}
lcg.drop(1)
f
}).foreach(Await.ready(_, Duration.Inf))
}
}
これで並列処理でも安全にLCGインスタンスを扱うことができます.
作成したLCGをエンカウント処理で使う
ここまで作成したLCGクラスを使って3世代の野生乱数の処理を書いてみましょう. 問題を簡単にするために次のような仕様で処理を書くことにします.
- seedは
0x00000000
- 検索する消費数の範囲は 1 〜 100000
- CDSの個体値がVのものを検索
- 並列処理する
…と簡単にしたと言ってもなかなか複雑そうなプログラムになりそうですが, ここでもScalaのコレクションライブラリの力が発揮されます. コードを見てみましょう*3.
class LCG(seed: Int, a: Int, b: Int) extends Iterator[Int] { ... }
class TemperedLCG(seed: Int, a: Int, b: Int) extends LCG(seed, a, b) { ... }
case class Encounter(frame: Int, slot: Int, level: Int, nature: Int, pid: Int, ivs: Seq[Int], item: Int) {
override def toString(): String = {
s"frame: $frame, slot: $slot, level: $level, nature: $nature, pid: ${pid.toHexString}, ivs: ${ivs.mkString("(", ", ", ")")}, item: $item"
}
}
class EncounterBuilder(wantedIvs: Seq[Range]) {
var frame: Int = 0
var slot: Int = 0
var level: Int = 0
var nature: Int = 0
var pid: Int = 0
var ivs: Seq[Int] = Nil
var item: Int = 0
def frame(frame: Int): Unit = this.frame = frame
def slot(slot: Int): Unit = this.slot = slot
def level(level: Int): Unit = this.level = level
def nature(nature: Int): Unit = this.nature = nature
def pid(pid: Int): Unit = this.pid = pid
def ivs(ivs: Seq[Int]): Unit = this.ivs = ivs
def item(item: Int): Unit = this.item = item
def result(): Option[Encounter] = {
val isValidIvs = (wantedIvs zip ivs).forall({ case (range, iv) => range.contains(iv) })
if (isValidIvs) Some(Encounter(frame, slot, level, nature, pid, ivs, item))
else None
}
}
object Wandbox {
def printRands(lcg: LCG, n: Int) = println(for (i <- 1 to n) yield lcg.next())
def getPID(lid: Int, hid: Int): Int = lid.toInt | (hid.toInt << 16)
def isValidPID(pid: Int, nature: Int): Boolean = java.lang.Integer.remainderUnsigned(pid, 25) == nature
def getIVs(rand: Int): (Int, Int, Int) = ((rand >> 10) & 0x1F, (rand >> 5) & 0x1F, rand & 0x1F)
def searchEncounter(lcg: LCG, builder: EncounterBuilder, frame: Int) = {
builder.frame(frame)
builder.slot(lcg.next(100))
builder.level(lcg.next())
val nature = lcg.next(25)
builder.nature(nature)
val pid: Int = lcg
.grouped(2)
.map(iter => getPID(iter.head, iter.tail.head))
.find(pid => isValidPID(pid, nature))
.get
builder.pid(pid)
val (b, a, h) = getIVs(lcg.next())
val (d, c, s) = getIVs(lcg.next())
builder.ivs(Vector(h, a, b, c, d, s))
lcg.drop(5)
builder.item(lcg.next(100))
builder.result()
}
def main(args: Array[String]): Unit = {
val lcg = new TemperedLCG(0x00000000, 0x41c64e6d, 0x6073)
val wantedIvs = Vector(
0 to 31,
0 to 31,
0 to 31,
31 to 31,
31 to 31,
31 to 31
)
val maxFrame = 100000
val results: Seq[Encounter] = (1 to maxFrame).map(frame => {
val forkedLcg = lcg.fork()
val builder = new EncounterBuilder(wantedIvs)
val f: Future[Option[Encounter]] = Future {
searchEncounter(forkedLcg, builder, frame)
}
lcg.drop(1)
f
}).map(Await.result(_, Duration.Inf)).flatten
results.foreach(println _)
}
}
出力
frame: 15506, slot: 40, level: 7292, nature: 0, pid: d000755f, ivs: (6, 27, 31, 31, 31, 31), item: 38
frame: 15530, slot: 86, level: 27437, nature: 0, pid: d000755f, ivs: (6, 27, 31, 31, 31, 31), item: 38
frame: 15540, slot: 72, level: 47192, nature: 0, pid: d000755f, ivs: (6, 27, 31, 31, 31, 31), item: 38
frame: 31693, slot: 21, level: 43592, nature: 1, pid: 44b35699, ivs: (2, 19, 18, 31, 31, 31), item: 64
frame: 36963, slot: 73, level: 6376, nature: 9, pid: 8e20683d, ivs: (13, 7, 24, 31, 31, 31), item: 43
frame: 36983, slot: 85, level: 41274, nature: 9, pid: 8e20683d, ivs: (13, 7, 24, 31, 31, 31), item: 43
frame: 36993, slot: 90, level: 26931, nature: 9, pid: 8e20683d, ivs: (13, 7, 24, 31, 31, 31), item: 43
乱数列からエンカウントデータを生成する searchEncounter()
では, Builderパターンを用いています*4. PIDの決定では grouped()
メソッドを使い, コレクションを (LID, HID)
の組にすることで2つずつ乱数を消費しながら条件を満たすPIDの検索をしています.
Builderに注目すると, Builder自身に検索する個体の条件を持たせていることがわかります. これは result()
で使用していて, 条件を満たさなければエンカウントデータの代わりに条件を満たす個体が見つからなかったことを表わす None
を返しています. また条件が満たされているかの判定には zip
, forall
を使っています. Builderパターンを採用することで個体の生成, および個体のフィルタリング処理が searchEncounter()
からBuilderに切り離されることになります.
このように, Scalaのコレクションライブラリの力を借りることで乱数ツールのコードを綺麗に記述することができます 👍
おわりに
以上が Pokémon RNG Advent Calendar 2017 10日目「Scalaで乱数ツールを書く話」となります. いかがでしたでしょうか. この記事を読んでちょっとしたツールの作成でも綺麗なコードを意識して取り組むきっかけになれればと思います 😃
adventar.org
11日目は oupo さんの担当です!
参考