當前位置: 首頁> 最新文章列表> mt_srand 在遊戲開發中怎麼用於分配角色屬性,實現隨機但公平的設定?

mt_srand 在遊戲開發中怎麼用於分配角色屬性,實現隨機但公平的設定?

gitbox 2025-08-30
<span><span><span class="hljs-meta"><?php</span></span><span>
</span><span><span class="hljs-keyword">echo</span></span><span> <span class="hljs-string"><<<HTML
<!--
  這段註釋、函數與樣式只是為演示而存在,和正文無關。
  如果你僅需正文,請從下面的水平線之後開始閱讀。
-->
<style>
  body { font-family: system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; line-height: 1.65; max-width: 820px; margin: 2rem auto; padding: 0 1rem; }
  pre { overflow: auto; padding: .75rem; background: #f6f8fa; border-radius: 6px; }
  code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
</style>
<hr>

<h1>mt_srand 在遊戲開發中怎麼用於分配角色屬性,實現隨機但公平的設定?</h1>

<p><strong>摘要</strong>:<code>mt_srand()

說明:不要用time()直接播種,否則同秒內會“撞種”。不要把mt_srand()反複調用在一段分配流程裡——在一次分配過程開始時播種一次即可。

三、屬性點“總量固定”的公平分割:隨機切割法(Random Cut / Stars and Bars)

場景:有6 個屬性(力、敏、智、體、運、意志),總點數固定為30,每項需在[1,10] 區間。

 
/**
 * 將 totalPoints 按均勻隨機切成 n 份,滿足每份均 ≥ minVal、≤ maxVal,且總和不變。
 * 若限製過緊導致不可行,自動回退重嘗試(最多 retries 次)。
 */
function random_partition_constrained(int </span><span><span class="hljs-subst">$n</span></span><span>, int </span><span><span class="hljs-subst">$totalPoints</span></span><span>, int </span><span><span class="hljs-subst">$minVal</span></span><span>, int </span><span><span class="hljs-subst">$maxVal</span></span><span>, int </span><span><span class="hljs-subst">$retries</span></span><span> = 256): array {
    for (</span><span><span class="hljs-subst">$t</span></span><span> = 0; </span><span><span class="hljs-subst">$t</span></span><span> &lt; </span><span><span class="hljs-subst">$retries</span></span><span>; </span><span><span class="hljs-subst">$t</span></span><span>++) {
        // 先扣掉下限,剩餘 R 進行自由分配
        </span><span><span class="hljs-subst">$R</span></span><span> = </span><span><span class="hljs-subst">$totalPoints</span></span><span> - </span><span><span class="hljs-subst">$n</span></span><span> * </span><span><span class="hljs-subst">$minVal</span></span><span>;
        if (</span><span><span class="hljs-subst">$R</span></span><span> &lt; 0) throw new InvalidArgumentException('下限之和超過總點數');
        // 生成 n-1 個“切點”,對 [0, R] 進行均勻切割
        </span><span><span class="hljs-subst">$cuts</span></span><span> = [];
        for (</span><span><span class="hljs-subst">$i</span></span><span> = 0; </span><span><span class="hljs-subst">$i</span></span><span> &lt; </span><span><span class="hljs-subst">$n</span></span><span> - 1; </span><span><span class="hljs-subst">$i</span></span><span>++) { </span><span><span class="hljs-subst">$cuts</span></span><span>[] = mt_rand(0, </span><span><span class="hljs-subst">$R</span></span><span>); }
        sort(</span><span><span class="hljs-subst">$cuts</span></span><span>);
        </span><span><span class="hljs-subst">$parts</span></span><span> = [];
        </span><span><span class="hljs-subst">$prev</span></span><span> = 0;
        foreach (</span><span><span class="hljs-subst">$cuts</span></span><span> as </span><span><span class="hljs-subst">$c</span></span><span>) { </span><span><span class="hljs-subst">$parts</span></span><span>[] = </span><span><span class="hljs-subst">$c</span></span><span> - </span><span><span class="hljs-subst">$prev</span></span><span>; </span><span><span class="hljs-subst">$prev</span></span><span> = </span><span><span class="hljs-subst">$c</span></span><span>; }
        </span><span><span class="hljs-subst">$parts</span></span><span>[] = </span><span><span class="hljs-subst">$R</span></span><span> - </span><span><span class="hljs-subst">$prev</span></span><span>;
        // 加回下限,並檢查上限
        </span><span><span class="hljs-subst">$vals</span></span><span> = array_map(fn(</span><span><span class="hljs-subst">$x</span></span><span>) =&gt; </span><span><span class="hljs-subst">$x</span></span><span> + </span><span><span class="hljs-subst">$minVal</span></span><span>, </span><span><span class="hljs-subst">$parts</span></span><span>);
        if (max(</span><span><span class="hljs-subst">$vals</span></span><span>) &lt;= </span><span><span class="hljs-subst">$maxVal</span></span><span>) return </span><span><span class="hljs-subst">$vals</span></span><span>;
    }
    throw new RuntimeException('在約束下未能找到分割,請放寬上下限或增加總點數。');
}

// 示例:6 項屬性,總 30 點,每項 [1,10]
</span><span><span class="hljs-subst">$attrs</span></span><span> = random_partition_constrained(n: 6, totalPoints: 30, minVal: 1, maxVal: 10);
// </span><span><span class="hljs-subst">$attrs</span></span><span> 形如 [7, 4, 2, 6, 3, 8],總和恒为 30,且均在區間內

該方法具備三個優點:總和嚴格受控各項無偏(在約束可行的前提下)、實現簡潔。需要注意的是,當maxVal太小或minVal太大導致可行域很窄時,可能多次重採樣才命中。

四、偏好與稀有度:加權無偏抽取(Alias 或分段採樣)

如果不同屬性希望有不同“權重”(例如“運氣”稍稀有),可以按權重抽取額外的“加成點”。下面是簡單、易讀的分段採樣方案(嚴格按權重無偏):

 
/**
 * 根據權重數組返回索引(無偏)
 * @param array&lt;float&gt; </span><span><span class="hljs-subst">$weights</span></span><span> 非負,至少一項 &gt; 0
 */
function weighted_pick(array </span><span><span class="hljs-subst">$weights</span></span><span>): int {
    </span><span><span class="hljs-subst">$sum</span></span><span> = array_sum(</span><span><span class="hljs-subst">$weights</span></span><span>);
    if (</span><span><span class="hljs-subst">$sum</span></span><span> &lt;= 0) throw new InvalidArgumentException('权重總和必须 &gt; 0');
    </span><span><span class="hljs-subst">$r</span></span><span> = mt_rand() / mt_getrandmax() * </span><span><span class="hljs-subst">$sum</span></span><span>; // [0, sum)
    </span><span><span class="hljs-subst">$acc</span></span><span> = 0.0;
    foreach (</span><span><span class="hljs-subst">$weights</span></span><span> as </span><span><span class="hljs-subst">$i</span></span><span> =&gt; </span><span><span class="hljs-subst">$w</span></span><span>) {
        </span><span><span class="hljs-subst">$acc</span></span><span> += </span><span><span class="hljs-subst">$w</span></span><span>;
        if (</span><span><span class="hljs-subst">$r</span></span><span> &lt; </span><span><span class="hljs-subst">$acc</span></span><span>) return </span><span><span class="hljs-subst">$i</span></span><span>;
    }
    return array_key_last(</span><span><span class="hljs-subst">$weights</span></span><span>); // 漂移保護
}

// 示例:給隨機分割的 6 項再附加 3 點“稀有加成”,權重偏向第 6 項(意志)
</span><span><span class="hljs-subst">$weights</span></span><span> = [1,1,1,1,1,2.5];
</span><span><span class="hljs-subst">$bonus</span></span><span> = [0,0,0,0,0,0];
for (</span><span><span class="hljs-subst">$k</span></span><span> = 0; </span><span><span class="hljs-subst">$k</span></span><span> &lt; 3; </span><span><span class="hljs-subst">$k</span></span><span>++) {
    </span><span><span class="hljs-subst">$idx</span></span><span> = weighted_pick(</span><span><span class="hljs-subst">$weights</span></span><span>);
    </span><span><span class="hljs-subst">$bonus</span></span><span>[</span><span><span class="hljs-subst">$idx</span></span><span>] += 1;
}

五、控制波動:使用“近似正態”的隨機(Box–Muller)

有時你希望“多數中庸、少數極端”。可用Box–Muller 將均勻分佈轉為正態分佈,再裁剪到區間內:

 
// 生成 ~N(0,1)
function randn(): float {
    do {
        </span><span><span class="hljs-subst">$u</span></span><span> = mt_rand() / mt_getrandmax();
        </span><span><span class="hljs-subst">$v</span></span><span> = mt_rand() / mt_getrandmax();
    } while (</span><span><span class="hljs-subst">$u</span></span><span> == 0.0); // 避免 log(0)
    return sqrt(-2.0 * log(</span><span><span class="hljs-subst">$u</span></span><span>)) * cos(2.0 * M_PI * </span><span><span class="hljs-subst">$v</span></span><span>);
}

// 在 [min, max] 上產生“鐘形”分佈
function randn_clamped(float </span><span><span class="hljs-subst">$min</span></span><span>, float </span><span><span class="hljs-subst">$max</span></span><span>, float </span><span><span class="hljs-subst">$mean</span></span><span>, float </span><span><span class="hljs-subst">$std</span></span><span>): float {
    </span><span><span class="hljs-subst">$x</span></span><span> = </span><span><span class="hljs-subst">$mean</span></span><span> + </span><span><span class="hljs-subst">$std</span></span><span> * randn();
    return max(</span><span><span class="hljs-subst">$min</span></span><span>, min(</span><span><span class="hljs-subst">$max</span></span><span>, </span><span><span class="hljs-subst">$x</span></span><span>));
}

做法:先用隨機切割保證“總量公平”,再對每個屬性加上一個零和的正態微調(正負相抵,保持總和不變),以塑造“多數中庸、少量突出”的體感。

六、多人競爭的“公平分配”:先洗牌再發放

例如4 名玩家搶4 套“預生成”的屬性模板,可以:

  1. matchId為整局播種;
  2. 預生成4 套模板;
  3. 按玩家入場順序的哈希進行Fisher–Yates 洗牌,一人一套,避免“座位優勢”。
 
// Fisher–Yates 洗牌:無偏
function shuffle_unbiased(array &amp;</span><span><span class="hljs-subst">$arr</span></span><span>): void {
    </span><span><span class="hljs-subst">$n</span></span><span> = count(</span><span><span class="hljs-subst">$arr</span></span><span>);
    for (</span><span><span class="hljs-subst">$i</span></span><span> = </span><span><span class="hljs-subst">$n</span></span><span> - 1; </span><span><span class="hljs-subst">$i</span></span><span> &gt; 0; </span><span><span class="hljs-subst">$i</span></span><span>--) {
        </span><span><span class="hljs-subst">$j</span></span><span> = mt_rand(0, </span><span><span class="hljs-subst">$i</span></span><span>);
        [</span><span><span class="hljs-subst">$arr</span></span><span>[</span><span><span class="hljs-subst">$i</span></span><span>], </span><span><span class="hljs-subst">$arr</span></span><span>[</span><span><span class="hljs-subst">$j</span></span><span>]] = [</span><span><span class="hljs-subst">$arr</span></span><span>[</span><span><span class="hljs-subst">$j</span></span><span>], </span><span><span class="hljs-subst">$arr</span></span><span>[</span><span><span class="hljs-subst">$i</span></span><span>]];
    }
}

// 預生成模板 + 洗牌分配
</span><span><span class="hljs-subst">$seed</span></span><span> = seed_from_context('ROOM-9F2A', 12, 'ALL'); // 用 ALL 代表整局
mt_srand(</span><span><span class="hljs-subst">$seed</span></span><span>);
</span><span><span class="hljs-subst">$templates</span></span><span> = [];
for (</span><span><span class="hljs-subst">$t</span></span><span> = 0; </span><span><span class="hljs-subst">$t</span></span><span> &lt; 4; </span><span><span class="hljs-subst">$t</span></span><span>++) {
    </span><span><span class="hljs-subst">$templates</span></span><span>[] = random_partition_constrained(6, 30, 1, 10);
}
shuffle_unbiased(</span><span><span class="hljs-subst">$templates</span></span><span>);
// 之后按玩家顺序一人取一個模板(或用玩家ID再做一次穩定洗牌)

七、驗證“長期公平”的做法

上線前做Monte Carlo 驗證:

  1. 固定一組上下文集合(不同玩家/房間/賽季),跑10w 次;
  2. 統計每個屬性的均值、方差、命中上下限的頻率;
  3. 對權重選擇做卡方檢驗(或至少做頻率差距的置信區間估計);
  4. 檢查是否存在相關性(例如某屬性與玩家ID的不期望耦合)。
 
// 粗略統計均值(示意)
function simulate_means(int </span><span><span class="hljs-subst">$runs</span></span><span> = 100000): array {
    </span><span><span class="hljs-subst">$sum</span></span><span> = array_fill(0, 6, 0.0);
    for (</span><span><span class="hljs-subst">$i</span></span><span> = 0; </span><span><span class="hljs-subst">$i</span></span><span> &lt; </span><span><span class="hljs-subst">$runs</span></span><span>; </span><span><span class="hljs-subst">$i</span></span><span>++) {
        mt_srand(</span><span><span class="hljs-subst">$i</span></span><span>); // 不同種子
        </span><span><span class="hljs-subst">$vals</span></span><span> = random_partition_constrained(6, 30, 1, 10);
        for (</span><span><span class="hljs-subst">$k</span></span><span> = 0; </span><span><span class="hljs-subst">$k</span></span><span> &lt; 6; </span><span><span class="hljs-subst">$k</span></span><span>++) { </span><span><span class="hljs-subst">$sum</span></span><span>[</span><span><span class="hljs-subst">$k</span></span><span>] += </span><span><span class="hljs-subst">$vals</span></span><span>[</span><span><span class="hljs-subst">$k</span></span><span>]; }
    }
    return array_map(fn(</span><span><span class="hljs-subst">$x</span></span><span>) =&gt; </span><span><span class="hljs-subst">$x</span></span><span> / </span><span><span class="hljs-subst">$runs</span></span><span>, </span><span><span class="hljs-subst">$sum</span></span><span>);
}

八、工程與安全注意

  • 一次流程一次播種:避免在同一流程中多次mt_srand()導致相關性。
  • 不要用mt_rand()做安全用途:涉及經濟價值(開箱、交易、競賽排名)或反作弊判定時,改用random_int() (CSPRNG)。
  • 跨語言一致性:服務端和客戶端若都參與隨機,務必統一算法與參數,或只在服務端做隨機並下發結果。
  • 版本固化:種子混入$salt='v1'一類的“版本號”以便將來調整算法而不破壞舊回放。

九、整合示例(可複現、總量固定、帶權重微調)

 
function assign_attributes(string </span><span><span class="hljs-subst">$matchId</span></span><span>, int </span><span><span class="hljs-subst">$season</span></span><span>, string </span><span><span class="hljs-subst">$playerId</span></span><span>): array {
    // 1) 播種
    mt_srand(seed_from_context(</span><span><span class="hljs-subst">$matchId</span></span><span>, </span><span><span class="hljs-subst">$season</span></span><span>, </span><span><span class="hljs-subst">$playerId</span></span><span>, 'attr-v1'));

    // 2) 基礎分割:6 項、總 30、每項 [1, 10]
    </span><span><span class="hljs-subst">$attrs</span></span><span> = random_partition_constrained(6, 30, 1, 10);

    // 3) 稀有加成:偏好第 6 項
    </span><span><span class="hljs-subst">$weights</span></span><span> = [1,1,1,1,1,2.5];
    for (</span><span><span class="hljs-subst">$i</span></span><span> = 0; </span><span><span class="hljs-subst">$i</span></span><span> &lt; 3; </span><span><span class="hljs-subst">$i</span></span><span>++) { </span><span><span class="hljs-subst">$attrs</span></span><span>[weighted_pick(</span><span><span class="hljs-subst">$weights</span></span><span>)] += 1; }

    // 4) 零和微調(保持總和不变,塑造“多數中庸”)
    </span><span><span class="hljs-subst">$mean</span></span><span> = array_sum(</span><span><span class="hljs-subst">$attrs</span></span><span>) / count(</span><span><span class="hljs-subst">$attrs</span></span><span>);
    </span><span><span class="hljs-subst">$delta</span></span><span> = 0.0;
    for (</span><span><span class="hljs-subst">$k</span></span><span> = 0; </span><span><span class="hljs-subst">$k</span></span><span> &lt; count(</span><span><span class="hljs-subst">$attrs</span></span><span>); </span><span><span class="hljs-subst">$k</span></span><span>++) {
        </span><span><span class="hljs-subst">$adj</span></span><span> = (int) round(randn() * 0.6); // 小幅波動,標準差可調
        </span><span><span class="hljs-subst">$attrs</span></span><span>[</span><span><span class="hljs-subst">$k</span></span><span>] += </span><span><span class="hljs-subst">$adj</span></span><span>;
        </span><span><span class="hljs-subst">$delta</span></span><span> += </span><span><span class="hljs-subst">$adj</span></span><span>;
    }
    // 抵消總和偏移(把舍入誤差勻回去)
    while (</span><span><span class="hljs-subst">$delta</span></span><span> != 0) {
        </span><span><span class="hljs-subst">$idx</span></span><span> = mt_rand(0, count(</span><span><span class="hljs-subst">$attrs</span></span><span>)-1);
        if (</span><span><span class="hljs-subst">$delta</span></span><span> &gt; 0 &amp;&amp; </span><span><span class="hljs-subst">$attrs</span></span><span>[</span><span><span class="hljs-subst">$idx</span></span><span>] &gt; 1) { </span><span><span class="hljs-subst">$attrs</span></span><span>[</span><span><span class="hljs-subst">$idx</span></span><span>]--; </span><span><span class="hljs-subst">$delta</span></span><span>--; }
        elseif (</span><span><span class="hljs-subst">$delta</span></span><span> &lt; 0) { </span><span><span class="hljs-subst">$attrs</span></span><span>[</span><span><span class="hljs-subst">$idx</span></span><span>]++; </span><span><span class="hljs-subst">$delta</span></span><span>++; }
    }

    // 5) 裁剪區間(如被裁剪,等額從/向其他項借还,保持總和)
    </span><span><span class="hljs-subst">$min</span></span><span> = 1; </span><span><span class="hljs-subst">$max</span></span><span> = 12;
    </span><span><span class="hljs-subst">$sumBefore</span></span><span> = array_sum(</span><span><span class="hljs-subst">$attrs</span></span><span>);
    </span><span><span class="hljs-subst">$attrs</span></span><span> = array_map(fn(</span><span><span class="hljs-subst">$v</span></span><span>) =&gt; max(</span><span><span class="hljs-subst">$min</span></span><span>, min(</span><span><span class="hljs-subst">$max</span></span><span>, </span><span><span class="hljs-subst">$v</span></span><span>)), </span><span><span class="hljs-subst">$attrs</span></span><span>);
    </span><span><span class="hljs-subst">$sumAfter</span></span><span> = array_sum(</span><span><span class="hljs-subst">$attrs</span></span><span>);
    // 歸還或補齊
    </span><span><span class="hljs-subst">$diff</span></span><span> = </span><span><span class="hljs-subst">$sumBefore</span></span><span> - </span><span><span class="hljs-subst">$sumAfter</span></span><span>;
    while (</span><span><span class="hljs-subst">$diff</span></span><span> != 0) {
        </span><span><span class="hljs-subst">$i</span></span><span> = mt_rand(0, count(</span><span><span class="hljs-subst">$attrs</span></span><span>)-1);
        if (</span><span><span class="hljs-subst">$diff</span></span><span> &gt; 0 &amp;&amp; </span><span><span class="hljs-subst">$attrs</span></span><span>[</span><span><span class="hljs-subst">$i</span></span><span>] &lt; </span><span><span class="hljs-subst">$max</span></span><span>) { </span><span><span class="hljs-subst">$attrs</span></span><span>[</span><span><span class="hljs-subst">$i</span></span><span>]++; </span><span><span class="hljs-subst">$diff</span></span><span>--; }
        elseif (</span><span><span class="hljs-subst">$diff</span></span><span> &lt; 0 &amp;&amp; </span><span><span class="hljs-subst">$attrs</span></span><span>[</span><span><span class="hljs-subst">$i</span></span><span>] &gt; </span><span><span class="hljs-subst">$min</span></span><span>) { </span><span><span class="hljs-subst">$attrs</span></span><span>[</span><span><span class="hljs-subst">$i</span></span><span>]--; </span><span><span class="hljs-subst">$diff</span></span><span>++; }
    }
    return </span><span><span class="hljs-subst">$attrs</span></span><span>;
}

十、結語

mt_srand()的“種子”與對局上下文綁定,可得到可複現的偽隨機;用隨機切割、權重採樣與零和微調等手法,就能在滿足約束的前提下構造隨機但公平的屬性分配。若涉及價值博弈或強對抗場景,考慮切換到random_int()等密碼學安全的隨機源,同時記錄“算法版本+ 種子+ 輸入”,為爭議處理與觀戰回放提供證據。

HTML;