サイトトップ

Director Flash 書籍 業務内容 プロフィール

Adobe Flash CS3 Professional ActionScript 3.0

□Tech 01 ランダムな値を取出す

サイコロを振ったり、おみくじを引いたり、占いやクイズの出題、あるいは敵の弾丸を発射させるなど、ランダムに何かを起こしたいことは少なくありません。その基本的なテクニックを、ふたつご紹介します。

01-01 ランダムな整数を得る
まず、ランダムに一定の範囲の整数を得る方法です。サイコロであれば、もちろん1から6までの整数になります。ランダムな値を得るには、Math.random()メソッドを使います。メソッドから返される値は、0以上1未満の浮動小数値です。したがって、ある範囲の整数を得たいときには、その最大値と最小値の整数をもとに、つぎの式で算出します。

Math.floor(Math.random()*(最大値-最小値+1))+最小値

サイコロであれば、Math.random()メソッドの戻り値にほしい整数(1〜6)の数6(= 6-1+1)を掛け合わせ、小数点以下を切捨てたうえで、最小値の1を足すということです。

Tips Tech01-001■Math.round()とMath.ceil()メソッド
小数点以下の数値を丸めて整数にするメソッドには、四捨五入のMath.round()や切上げのMath.ceil()もあります。しかし、これらのメソッドでランダムな整数を得ようとすると、問題になることがあります。たとえば、つぎのMath.round()メソッドを使った式で、0から2までのランダムな整数が算出できます。

Math.round(Math.random()*2)

しかし、この式では、0や2の出る倍の確率で1が発生します。Math.random()*2の結果が0以上0.5未満のとき整数0、1.5以上2.0未満であれば整数2となるのに対して、0.5以上1.5未満の範囲の値が整数1を導くからです(図Tech01-001)。

図Tech01-001■Math.round()メソッドでは両端の整数の出る確率が半分になる

0と2になる値の範囲は0.5、他の整数(1)は1.0の幅がある。

上記の式のメソッドMath.round()Math.ceil()に替えると、整数1と2がほとんど同じ確率で得られます。ただし、Math.random()メソッドの戻り値は0以上1未満ですので、きわめて小さい確率とはいえ、0の発生する可能性が排除できません(図Tech01-002)。

Math.ceil(Math.random()*2)
図Tech01-002■Math.ceil()メソッドでは0の発生する可能性が排除できない

Math.random()メソッドの戻り値は0以上1未満なので、0が返された場合は切上げても0になる。ただし、その発生する可能性はきわめて少ない。

Maniac! Tech01-001■Math.round()とMath.ceil()メソッドを使った問題の回避
前掲Tips Tech01-001のランダムな整数をMath.round()Math.ceil()メソッドにより得ようとする場合の問題は、少し手を加えれば回避することができます。まず、Math.round()メソッドついては、予め値に0.5を加えれば、四捨五入がいわば九捨零入となり、切捨てと同じ結果になります。

また、Math.ceil()メソッドで切上げたい場合には、Math.random()メソッドの戻り値が0だったときに条件判定で最大値の整数にすればよいでしょう。もっとも、Math.floor()メソッドを使う方がシンプルですので、あえてこれらのメソッドを用いる必要性は少ないと考えられます。


Maniac! Tech01-002■[ヘルプ]のMath.random()メソッドの説明
[ヘルプ]の[ActionScript 3.0コンポーネントリファレンスガイド]におけるMath.random()メソッドの説明は、日本語としていささか意味がとりづらく感じられます(図Tech01-003)。

図Tech01-003■[ヘルプ]のMath.random()メソッドの説明

説明のとくに後段が、理解しにくい。

以下は英文ドキュメントから、筆者が邦訳しました。とくにわかりにくかった後段の意味は、Math.random()メソッドにもランダムな値を算出するロジックがあるということです。そうであれば、返される値にも何らかのパターンがあり、真の乱数ではないことになります。説明では、それを疑似乱数と表現しています。

0≦n<1となる疑似乱数nを返します。その数値は、非公開の仕様により計算されます。計算にはランダムでない要素が含まれざるを得ませんので、疑似乱数とされます。

必要とする整数の最大値と最小値を引数に渡して、ランダムな整数を返す関数は、たとえば以下のように定義できます。ふたつの引数の最大値、最小値の順序は、気にする必要はありません。関数本体の中でその大小を判別しています。

function xGetRandomInt(n0:int=0, n1:int=1):int {
  var nMax:int = Math.max(n0, n1);
  var nMin:int = Math.min(n0, n1);
  var nRandom:int = Math.floor(Math.random()*(nMax-nMin+1))+nMin;
  return nRandom;
}

引数にはデフォルト値を指定しましたので、引数がなければ0か1かをランダムに返します。また、引数をひとつ指定すると、その整数値から1までのランダムな整数が返ります。


01-02 配列エレメントをランダムに並べ替える
つぎに、一定の範囲からランダムに、かつ重複なく値を取出したい場合があります。たとえば、用意した問題100問から、ランダムに30問を出題するという場合です。30問の中に、もちろん同じ問題が含まれてはいけません。この場合の考え方と処理の手法についてご説明します。

カードをシャッフルして配る ー 初めに運命を決める
出題済みの問題番号をリストしておけば、重複しているかどうかチェックができます。すると、こんな手順になるでしょう。(1)問題番号をランダムにひとつ取出します。(2)その番号が出題済みリストにあるかどうか調べます。(3)すでに出題されていたら(1)に戻ります。(4)まだ出題されていなかったらその番号を採用し、出題済みリストに加えます(図Tech01-004)。

図Tech01-004■重複の有無をリストで確かめる

リストの数が増えると、重複で戻る確率が高くなる。

この考え方は、論理的に誤った点はありません。しかし、出題済みのリストが増えると、ランダムな番号が重複する確率は高まります。そうすると、初めに戻ってランダムな数を引き直す頻度も増えるでしょう。いってみれば、ビンゴゲームで、1度引いた玉を、もとのかごに戻すようなものだからです。その分、処理効率は低下します。

もっとスマートな方法は、初めに運命を決めてしまうことです。カードゲームでは、プレーヤーにランダムにカードを配る必要があります。しかし、ひとりひとりのプレーヤーに、カードの山から1枚1枚ランダムに抜取ってもらったりはしません。初めにカードをシャッフルしたら、頭から順に各プレーヤーに配ります。理屈では、シャッフルし終えた時点で、各プレーヤーの手札は決まります。だからといって、ランダムでないとか不公正ということにはなりません。

このカードを配る仕組みは、配列を使うと実現しやすいでしょう。つまり、母数となる値を配列エレメントに入れて、その順序をシャッフルしたら、頭(インデックス0)から順に取出せばよいのです(図Tech01-005)。シャッフルが終わった時点で、運命は決まります。

図Tech01-005■初めにシャッフルして運命を決める

配列に入れた値をランダムに並べ替えて、頭から順に取出せばよい。

[*イラスト候補●Tech01-001] 1: 初めに運命を決める。
または
2: シャッフルしたら順番に配る。

配列をシャッフルする関数
配列エレメントをランダムに並べ替えるための関数を定義してみましょう(スクリプトTech01-001)。関数名はxShuffleArray()とし、引数には並べ替える対象の配列を渡します。配列は参照渡し(Column 06「値と参照」参照)ですから、とくに戻り値は必要ありません。

スクリプトTech01-001■配列エレメントをランダムに並べ替える関数

// フレームアクション
function xShuffleArray(my_array:Array):void {
  var i:int = my_array.length;
  while (i) {
    var nRandom:int = Math.floor(Math.random()*(i--));
    var temp:Object = my_array[i];
    my_array[i]=my_array[nRandom];
    my_array[nRandom]=temp;
  }
}

この並べ替えのやり方は、手元のカードからランダムに1枚ずつ抜き取り、テーブルの上に順に積んでいくのと同じです。もっとも、カードのように手元とテーブルのふたつに山を分けず、ひとつの配列の中のエレメントを入替える処理になっているという違いはあります。

配列からエレメントをランダムに抜出す処理は、whileループで行っています。継続条件には、配列の長さを初期値として代入したローカル変数iが指定されています。変数iは、ループ処理の中で1ずつ減算(デクリメント)されますので、値が0になるとループを抜けます。

ランダムな整数の算出には、変数iがポストデクリメント(Tips 09-009「プリインクリメントとポストインクリメント」参照)で掛合わされていますので、0からi-1までの整数が返されます。そのインデックスから抜出したエレメントをインデックスiに設定しているのが、つづく3行のステートメントです。

この3行は、ふたつの値を入替える場合の定石といえる処理です。一時避難用の変数(temp)を宣言し、一方の値を代入します。そして、他方の値で、その一方の値を上書きしたあとに、一時避難した値を他方に上書きしています。なお、配列エレメントにはあらゆる値が入れられますので、一時避難用変数(temp)のデータ型はObjectで指定しました。

[*イラスト候補●Tech01-002] カードをランダムに抜いては、テーブルの上に積んでいく。

ランダムの偏り
前掲スクリプトTech01-001の処理で、ランダムな整数値を以下のように計算しようとする人がいます。これは、配列の長さがたとえば10であったら、毎回0から9までのランダムな整数を計算して、エレメントを入替えるということになります。

var i:int = my_array.length;
var n:int = i;
while (i) {
  var nRandom:int = Math.floor(Math.random()*(n));

たとえを変えて、クラスの席替えを考えてみましょう。普通は席に番号をつけ、その番号の書かれたくじを全員が引いて、席を決めるといった方法を採ります。前掲スクリプトTech01-001の処理は、多少プロセスが異なるものの、結果としては同じになります。

しかし、上記の毎回配列の長さ(n)により(0からn-1までの)ランダムな整数を計算するというやり方は、席の番号の書かれたくじを引いたあと、またそのくじを箱に戻すようなものです。たとえば、ある生徒が1番を引いて、1番の席に座ったとします。でも、くじを箱に戻しますので、あとの誰かがまた1番を引くかもしれません。すると、その人と席を交換しますから、最後のひとりがくじを引き終わるまで、誰も席が確定しないことになります。

これだけでしたら、多少無駄な感じがするという程度のことでしょう。しかし、このやり方の本当の問題は、ランダムな並び順に偏りが生じるかもしれないということです。簡単にするため、配列の長さが3だとします。つまり、座席は3つです。すると、くじを引いて戻すときの結果は、3人とも1番から3番までの3つの場合がありえます。すべての場合の数を数えれば、33で27とおりです。

くじを戻さなければ、ひとり引くたびにくじが減りますので、3×2×1の6とおりです。3人の並び方は、実際には6とおりしかありません。ですから、前記の方法の27とおりというのは、この6とおりのうちのどれかが単に何度も出てくるだけのことです。しかし、27を6で割ると、3余ります。ということは、6とおりのうち、半分の3とおりは、他の3とおりより大目に出ているはずです。

つまり、出やすい並び順と出にくい並び順とがあり、それぞれの発生確率が等しくないということです。ランダムの出方に偏りが生じるというのは問題です。したがって、前掲スクリプトTech01-001のようにランダムな整数の範囲を毎回減らし、くじを減らすのとのと同様に処理する必要があるのです。

Maniac! Tech01-003■ループ処理の最適化
スクリプトTech01-001について、もっと無駄を省いて最適化する余地がないでしょうか。ふたつだけ考察しておきます。

まず、配列エレメントを入替えるとき、ランダムな整数(nRandom)と入替え先インデックス番号(i)が同じ値になるかもしれません。その場合には、入替えの処理をする必要はないはずです。この考え方は、間違っていません。

しかし、ふたつの整数値が同じかどうかを毎回調べるのと、無条件に入替えを行ってしまうのとでは、とくに配列の長さが大きいときはほとんど処理速度は変わりません。したがって、小さい配列を大量に処理するといった場合でなければ、処理をシンプルにした方がよいと判断しました。

つぎに、whileステートメントの継続条件です。変数iは0でループを抜けますので、1が最後に処理される変数値です。このとき、ランダムな整数は必ず0になります。つまり、最後のエレメント(インデックスは0)の行き先を考えたとき、他のエレメントはすでに位置が決まっていますから、その場所は結局動けないことになるのです。

そこで、継続条件をi-1に変えてみても、1回処理が減る代わりに、毎回継続条件で引き算を行います。筆者のテストでは、配列の長さが大きいときは、むしろスクリプトTech01-001の方がわずかに速いという結果でした。したがって、ここでもシンプルな処理を採用しました。

[Prev/Next]


作成者: 野中文雄
作成日: 2008年7月10日


Copyright © 2001-2008 Fumio Nonaka.  All rights reserved.