わたくしのような凡人が決して立ち入ってはならない、確率を操る世界へ一歩だけ足を踏み入れてみたいと思います。


前もって固定の値を決めておかずに、実行させた時にランダムな値を得たい時ってあるよね~ (あるあるぅ~)
ハイ、そんな時にはこのrand()関数が使えるのさ。

関数仕様はこんな感じ
int rand(int $min, int $max);



こう書けば1~10のうちいずれかを返してくれるぜぃ
$num = rand(1, 10);



条件式として0か1の真偽値を返すようにすれば50%の確率で条件内容が処理されるわけさ
if (rand(0, 1)) {
    echo "夏だぜぃ!";
}



応用として、1/5の確率ならこう書けばよかろう
if (rand(1, 5) === 1) {

}



3/5ならこうか
if (rand(1, 5) > 2) {

}



ではこの関数がどれだけ乱れるかを実験してみるぞなもし

■乱数実験1 確率と回数
<?php

// 100回
$result = simulate(100);
print_s($result);

// 10000回
$result = simulate(10000);
print_s($result);

// 1000000回
$result = simulate(1000000);
print_s($result);


function simulate($n) {

    $result = array_fill(1, 10, 0); //配列添え字1~10を0初期化

    for ($i=0; $i<$n; $i++) { // $n回繰り返す

        //1~10のいずれが出たかをカウント
        $result[rand(1, 10)]++;
    }

    return $result;
}

function print_s($array) {

    printf("[%d回シミュレート]\n", array_sum($array));

    foreach ($array as $i => $count) {

        printf("%2d-> %6d\n", $i, $count);
    }
}



・結果
[100回シミュレート]
 1->     16
 2->      9
 3->     18
 4->      5
 5->      8
 6->     10
 7->      7
 8->      9
 9->     12
10->      6
[10000回シミュレート]
 1->    966
 2->    985
 3->    977
 4->   1027
 5->    975
 6->   1003
 7->    982
 8->   1055
 9->    977
10->   1053
[1000000回シミュレート]
 1-> 100022
 2-> 100006
 3-> 100055
 4->  99995
 5-> 100026
 6->  99988
 7-> 100025
 8->  99956
 9-> 100010
10->  99917



このように、回数多くやった方がより均等にバラつかせられることが分かる。
つまりできるだけいいサンプル採るには多めにやれってことだね。
これはゲームとかシミュレーションに使えるぞな!



■乱数実験2 確率を割り当てる

例えば10%の確率でAになって、33%はBになって、残りはCになる・・・
みたいな感じで割り当てればよさそうなので、百分率という名のとおり、1~100が出るようにすれば割り当てがうまくできそうだ。

つまりザッとこんな感じで書けるだろう
$num = rand(1, 100);

if ($num <= 10) {  // 10%は
    echo 'A';
} else if ($num <= 10 + 33) { // 33%は
    echo 'B';
} else {           // 残りは
    echo 'C';
}



ではこれが果たして想定どおりの確率になっているかを確かめる。
より精度が上がるよう1000000回試す

<?php
define("SIMULATE_NUM", 1000000);

for ($i=0; $i<SIMULATE_NUM; $i++) {
    $result[simulate()]++;
}

asort($result);
foreach ($result as $Character => $count) {
    printf("%s-> %2.3f\n", $Character, $count / SIMULATE_NUM * 100);
}

// 設定確率でAかBかCを返す
function simulate() {

    $num = rand(1, 100);

    if ($num <= 10) {
        $result = 'A';
    } else if ($num <= 10 + 33) {
        $result = 'B';
    } else {
        $result = 'C';
    }

    return $result;
}



・結果
A-> 9.998
B-> 33.001
C-> 57.000



割とイケてる結果が出たんじゃないでしょうか?
何度やってもだいたいこれくらいの誤差で表示できました。



■乱数実験3 シミュレーション

最後にちょいとしたジャンケンのシミュレーションをば

ジャンケンは2人でやるものとして、人間クラスがまず必要。
あとはこの両者を登録して戦わせる為のバトルクラスを作ればうまくいきそうだね。

ってことで作って戦わせてみました。

<?php
define("GOO"  , "goo"  );
define("CHOKI", "choki");
define("PAA"  , "paa"  );


class Person
{
    private $_goo   = 33;
    private $_choki = 33;
    private $_paa   = 33;

    public function setGoo($per) {
        $this->_goo = $per;
    }
    public function setChoki($per) {
        $this->_choki = $per;
    }
    public function setPaa($per) {
        $this->_paa = $per;
    }
    //設定の確率でジャンケンを繰り出す
    public function janken() {
        $result = rand(1, 100);
        if ($result <= $this->_goo) {
            return GOO;
        } else if ($result <= $this->_goo + $this->_choki) {
            return CHOKI;
        } else {
            return PAA;
        }
    }
}


class Battle
{
    private $_person1;
    private $_person2;

    public function setPerson1($obj) {
        $this->_person1 = $obj;
    }
    public function setPerson2($obj) {
        $this->_person2 = $obj;
    }
    private function _judge($person1, $person2) {

        if ($person1 === GOO   && $person2 === CHOKI
         || $person1 === CHOKI && $person2 === PAA
         || $person1 === PAA   && $person2 === GOO) { // person1 win
            return '1win';
        } else if ($person1 === $person2) { // draw
            return 'draw';
        } else {
            return '2win';
        }
    }
    public function go() {
        return $this->_judge($this->_person1->janken(),$this->_person2->janken());
    }
}


// 人間オブジェクト1と2作成
$person1 = new Person();
$person2 = new Person();

// 第1バトルのタイトル表示
echo "Battle1): 1(33,33,34), 2(33,33,34)\n";
// 属性設定
$person1->setGoo(33);
$person1->setChoki(33);
$person1->setPaa(34);
$person2->setGoo(33);
$person2->setChoki(33);
$person2->setPaa(34);
// バトルオブジェクト作成
$battle = new Battle();
$battle->setPerson1($person1);
$battle->setPerson2($person2);
// バトル
for ($i=0; $i<1000000; $i++) {
    $result[$battle->go()]++;
}
// 結果表示
print_r($result);


// 第2バトル
echo "Battle2): 1(33,33,34), 2(10,10,80)\n";
$person2->setGoo(10);
$person2->setChoki(10);
$person2->setPaa(80);

$result = array();
for ($i=0; $i<1000000; $i++) {
    $result[$battle->go()]++;
}
print_r($result);


// 第3バトル
echo "Battle3): 1(33,33,34), 2(0,0,100)\n";
$person2->setGoo(0);
$person2->setChoki(0);
$person2->setPaa(100);

$result = array();
for ($i=0; $i<1000000; $i++) {
    $result[$battle->go()]++;
}
print_r($result);


// 第4バトル
echo "Battle4): 1(50,50,0), 2(0,50,50)\n";
$person1->setGoo(50);
$person1->setChoki(50);
$person1->setPaa(0);
$person2->setGoo(0);
$person2->setChoki(50);
$person2->setPaa(50);

$result = array();
for ($i=0; $i<1000000; $i++) {
    $result[$battle->go()]++;
}
print_r($result);



・結果
Battle1): 1(33,33,34), 2(33,33,34)
Array
(
    [draw] => 333298
    [2win] => 333196
    [1win] => 333506
)
Battle2): 1(33,33,34), 2(10,10,80)
Array
(
    [draw] => 338097
    [1win] => 330884
    [2win] => 331019
)
Battle3): 1(33,33,34), 2(0,0,100)
Array
(
    [1win] => 329962
    [draw] => 340029
    [2win] => 330009
)
Battle4): 1(50,50,0), 2(0,50,50)
Array
(
    [2win] => 249812
    [1win] => 500372
    [draw] => 249816
)



この結果を見ますと、
  • グー・チョキ・パー平均的に出す人同士はやはり勝ち負けあいこすべて同程度
  • 片方だけが割合変えると、あいこは多くなるが勝ち負けは同程度
  • グー・チョキ・パーのうち、どれか1つを出さない者同士でジャンケンすると、かみ合わせいい方が倍勝てる
人間の場合はどれを最初に出すかだけ見ても癖があるし、感情動物なので熱くなると一定のパターンに陥ってそれが吉と出るか凶と出るかどちらかに傾くだろう。
つまり、ジャンケン強いと豪語する人に対しては何の感情も持たない乱数をうまく使えば対等な勝負ができそうだ。

ただ、ジャンケンは100万回も繰り返すことはないので、短期決戦、出たとこ勝負。
そう考えると、相手のクセを見抜いて勝つほうが確率が上がるだろう。