Viewer

牌山生成について

牌山生成の流れ

MJ Arcadeにおける、牌山とサイコロの目の決定方法について説明します。

【1】マッチング前
抽選により、「プレイヤー乱数シード」が決まります。

マッチング前

【2】各局の開始時
「全員のプレイヤー乱数シード」と、「不確定要素」を使って、牌山とサイコロの目を決めます。

各局の開始時

【3】試合終了
対局者に対して、「全員のプレイヤー乱数シード」が公開されます。
→牌山Viewerに公開された情報を入力すると、牌山を再現できます。

試合終了

牌山生成アルゴリズム

アルゴリズム解説

1 「プレイヤー乱数シード」と「不確定要素」から、「乱数の種」を生成する。
  • 「全員のプレイヤー乱数シード」を数値に変換し、小さい順に連結して640bitの値にする。
    ※三人打ちでは480bitの値
  • 640bitの値を不確定要素によって変化させ、乱数の種とする。
    (不確定要素は、毎局変化する値)
2 「乱数の種」を使って、「牌山」を作る。
  • 起家を起点に、「萬子」、「索子」、「筒子」、「字牌」の順に牌を並べ、仮の牌山を作る。
  • 仮の牌山を、「乱数の種」によって生成した「乱数」を使ってシャッフルする。
3 「乱数の種」を使って、「サイコロの目」を決める。

サンプルコード

©SEGA
無断複製・改変を禁じます。商用での利用は行わないでください。


// 座席位置
var Direction = 2;

// 牌山の計算
function calcYama(Pseed1,Pseed2,Pseed3,Pseed4,UEseed){

    // 各プレイヤーシード(文字列)を160bitの数値にデコードする
    var aPseed160 = string_to_u160x4(Pseed1,Pseed2,Pseed3,Pseed4);

    // プレイヤーシードを小さい順に並べて連結し、32bit*20の配列にする
    var aSeed32 = connect_seed(aPseed160);
    
    // 不確定要素でxorして、完成形のシードにする
    var aSeedComp = xor_seed(aSeed32,UEseed);

    // 牌山の初期値として順番にセット
    var yama = new Yama();

    // 完成したシードを使用して、ランダムに牌山全てを入れ替える
    var mt_random = new MtRandom(aSeedComp,20);
    for (j=0 ; j<136 ; ++j){
        var r = mt_random.rand();
	r%=136;
	yama.swap(r,j);
    }
    // サイコロの値を決める
    yama.setdice(mt_random.rand()%6,mt_random.rand()%6);

    return yama;
}

// 各プレイヤーシード(文字列)を160bitの数値にデコードする
function string_to_u160x4(seed1,seed2,seed3,seed4){
    var tbl=new Array();
    tbl[0]=string_to_u32(seed1);
    tbl[1]=string_to_u32(seed2);
    tbl[2]=string_to_u32(seed3);
    tbl[3]=string_to_u32(seed4);
    return tbl;
}
// 文字列をu32に変換する関数
function string_to_u32(seed){
    org_letter="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_";
    // 27文字×6bit = 162bit->32bit×5に変換
    // いったん2進数にする
    var allbit=new String();
    for (i=0 ; i<27 ; ++i){
        // 1文字分を2進数に変換
	c1=seed.charAt(i);
	for (j=0 ; j<64 ; ++j){
	    c2=org_letter.charAt(j);
	    if (c1==c2){
	        num="00000"+j.toString(2);
		allbit+=num.substring(num.length-6,num.length);
		break;
	    }
	}
    }
    var tbl=new Array(5);
    tbl[0]=parseInt(allbit.substring(0,32),2)>>>0;
    tbl[1]=parseInt(allbit.substring(32,64),2)>>>0;
    tbl[2]=parseInt(allbit.substring(64,96),2)>>>0;
    tbl[3]=parseInt(allbit.substring(96,128),2)>>>0;
    tbl[4]=parseInt(allbit.substring(128,160),2)>>>0;
    return (tbl);
}

// プレイヤーシードを小さい順に並べて連結し、32bit*20の配列にする
function connect_seed(tbl){
    for (i=0 ; i<4 ; i++){
        for (j=0 ; j<3 ; j++){
	    for (k=0 ; k<5 ; k++){
	        if (tbl[j][k] > tbl[j+1][k]){
		    // 各要素で小さい順に並べ替える
		    for (l=0 ; l<5 ; ++l){
		        var tmp = tbl[j][l];
			tbl[j][l] = tbl[j+1][l];
			tbl[j+1][l] = tmp;
		    }
		    break;
		}else if (tbl[j][k] < tbl[j+1][k]){
		    // 既に小さい順になっているので終了
		    break;
		}
	    }
	}
    }

    var seed64=new Array(20);
    for (j=0 ; j<5 ; j++){
	for (i=0 ; i<4 ; i++){
	    seed64[j*4+i]=tbl[i][j];
        }
    }
    return seed64;
}

// 不確定要素でxorして、完成形のシードにする
function xor_seed(seed640, seed5){
    for (i=0 ; i<20 ; i++){
        seed640[i]^=seed5;
    }
    return seed640;
}

// 32bitx32bitかけ算 結果は32bit部分のみ
function Mul32(num1, num2){
    var low1=num1&0xffff;
    var low2=num2&0xffff;
    var up1 =(num1/0x10000)>>>0;
    var up2 =(num2/0x10000)>>>0;
    return (low1*low2+((low1*up2*0x10000)>>>0)+((low2*up1*0x10000)>>>0))>>>0;
}

// MT乱数
var MtRandom = function( seed , length){
    var i = 1;
    var j = 0;
    var k;

    var tmp_seed = 19650218;
    var ix;

    this.MT_RANDOM_N = 624;

    this.index = 0;

    this.mt = new Array(this.MT_RANDOM_N);
    this.mt[ 0 ] = tmp_seed & 0xffffffff;
    for ( ix = 1 ; ix < this.MT_RANDOM_N ; ix++ ){ 
        var tmp1 = ((this.mt[ ix -1 ] ^ (this.mt[ ix -1 ] >>> 30))>>>0);
	var tmp2 = Mul32(tmp1,1812433253);
	var tmp3 = (tmp2>>>0)+ix;
	this.mt[ ix ] = tmp3>>>0;
    }

    var set_length = length;
    if (set_length < this.MT_RANDOM_N)
        set_length = this.MT_RANDOM_N;
    for ( k = set_length ; k ; k-- ){
        var tmp1 = ((this.mt[ i -1 ] ^ (this.mt[ i -1 ] >>> 30))>>>0);
	var tmp2 = Mul32(tmp1,1664525);
	var tmp3 = (this.mt[ i ] ^ tmp2)>>>0;
	this.mt[ i ] = (tmp3 + seed[ j ] + j)>>>0;

	if( ++i >= this.MT_RANDOM_N ){
	    this.mt[ 0 ] = this.mt[ this.MT_RANDOM_N -1 ];
	    i = 1;
	}

	if( ++j >= length ){
	    j = 0;
	}
    }

    for ( k = this.MT_RANDOM_N - 1 ; k!=0 ; k-- ){
        var tmp1 = ((this.mt[ i -1 ] ^ (this.mt[ i -1 ] >>> 30))>>>0);
	var tmp2 = Mul32(tmp1,1566083941);
	var tmp3 = (this.mt[i] ^ tmp2)>>>0;
	this.mt[ i ] = (tmp3 -i)>>>0;

	if( ++i >= this.MT_RANDOM_N ){
	    this.mt[ 0 ] = this.mt[ this.MT_RANDOM_N -1 ];
	    i = 1;
	}
    }
    this.mt[ 0 ] = 0x80000000;

    this.rand = function(){
        var MT_UPPER_MASK = 0X80000000;
	var MT_LOWER_MASK = 0x7fffffff;
	var MT_MATRIX_A = 0x9908b0df;
	var MT_RANDOM_M = 397;

	var idx = this.index;
	var read_idx = idx + MT_RANDOM_M;
	var next_idx = idx + 1;

	if (read_idx >= this.MT_RANDOM_N ){
	    read_idx -= this.MT_RANDOM_N;

	    if( next_idx >= this.MT_RANDOM_N ){
	        next_idx = 0;
	    }
	}

	var ret = (((this.mt[ idx ] & MT_UPPER_MASK)>>>0) | ((this.mt[ next_idx ] & MT_LOWER_MASK)>>>0))>>>0;
	ret = (((this.mt[ read_idx ] ^ (ret >>> 1))>>>0) ^ ((((- ((ret & 0x1)>>>0) )) & MT_MATRIX_A)>>>0))>>>0;

	this.mt[ idx ] = ret;
	this.index = next_idx;

	ret = (ret ^ (ret >>> 11))>>>0;
	ret = (ret ^ (((ret << 7) & 0x9d2c5680)>>>0))>>>0;
	ret = (ret ^ (((ret << 15) & 0xefc60000)>>>0))>>>0;
	ret = (ret ^ (ret >>> 18))>>>0;

	return ret;
    }
};

// 牌山
var Yama = function(){
    this.yama = new Array();
    for (i=0 ; i<136 ; ++i){
        this.yama[i]=i;
    }
    this.dice = new Array();
    this.dice[0]=0;
    this.dice[1]=0;
    this.swap = function(i,j){
        var tmp=this.yama[i];
	this.yama[i]=this.yama[j];
	this.yama[j]=tmp;
    }
    this.setdice = function(i,j) {
        this.dice[0]=i;
	this.dice[1]=j;
    };
}

プレイヤー乱数シード

抽選により決定する値です。
抽選は、プレイヤーが「ゲーム開始」または「コンティニュー」を選択したタイミングで行います。
生成方法については、セキュリティ上の理由により非公開となっています。

不確定要素

対局者の行動によって変化する値です。以下の行動によって、値が増加します。

  1. 対局者が打牌する
  2. 配牌

対局者の打牌による増加

打牌によって増える値=(何枚目の捨牌か?) *(牌種)

牌種 増加する値
数牌 数字をそのまま加算
字牌 東~北 1~4
白發中 5~7
【例1】3枚目の捨て牌が「中」
3 * 7 = 21
が加算されます。
3枚目の捨て牌が「中」
【例2】鳴きが入った場合
例1の「中」がポンされた場合、次の打牌も「3枚目の打牌」として扱います。
2筒を切った場合、 3 * 2 =6 が加算されます。
鳴きが入った場合

配牌での増加

配牌のタイミングで、「1」を加算します。
※親の天和や九種九牌などで、打牌がないまま局が終了し、不確定要素が前局と同じ値になるのを防ぐため。

乱数表示の確認方法

「プレイヤー乱数シード」と「不確定要素」は、筐体やMJ.NETで確認することができます。

筐体での確認方法

 対局中の「試合状況タブ」や「コンティニュー画面」で表示される、「乱数情報」をタッチしている間、「プレイヤー乱数シード」と「不確定要素」を確認することができます。

筐体での確認方法

MJ.NETでの確認方法

「対戦履歴」のページから、各試合の「乱数情報」を確認することができます。

アクセス方法
  • MJ.NET
  • プレイデータ
  • 対戦履歴
  • 乱数情報

※「対戦履歴」の確認は、MJ.NETの有料会員限定のサービスです。

「乱数情報」の表示制限

不正行為防止のため、筐体およびMJ.NETでの「乱数情報」の表示が制限される場合があります。

対局者全員の試合終了が確認できない場合

「対局途中で中断」したり、「通信障害が発生」する等した場合は、「乱数情報」を表示しません。
※先に終了したプレイヤーが乱数情報を悪用する恐れがあるため

Ver.A+以前のアルゴリズムを採用する場合

「MJ5Ver.A+以前のバージョンとマッチングした場合」および「店内対戦」では、「乱数情報」を表示しません。
※MJ5Ver.A以前の牌山生成アルゴリズムは、公開すると不正される恐れがあるため、非公開となっています。