前天提到亂數產生器,以一個看似單純卻聚集了數學精要的演算法自製亂數產生器。
昨天講了哈希雜湊,列舉了雜湊在遊戲裏能夠施展的各種能力。
那麼今天我們來試著讓MD5和亂數產生器合體,看看可以蹦出什麼樣的火花。
我們想想MD5做的是什麼?就是把我們看得懂的資料變成一串我們看不懂亂七八糟的字串。咦!?那不就是亂數的意思嗎!
我們給定一個基因整數,搭配一個名字,然後把這兩個參數用MD5.hash()一下,再從結果中取出一段數字當成亂數種子,這樣就可以得到一個專屬於這個名字的亂數產生器。這個亂數產生器只要給的基因整數與名字不變,那產出來的亂數數列就會一模一樣。
上面我講得很含糊,大家可能也聽得很迷濛,因為這個方法是我早年製作一個養寵物的遊戲時想出來的小把戲,一直很不好意思拿出來獻醜。不過現在臉皮厚了,下面我把當時實際使用的方法介紹給大家,希望能啟發同學們生出更多的好點子。
玩家在遊戲的世界中找到一隻寵物的實體時,這隻寵物就會有身高體重、技能力量、飲食偏好等各種屬性,事實上,這隻寵物連升級後的各項屬性也是在一出生的時候就已經決定好了。這樣的設計在遊戲中是怎麼做到的呢?
方法說穿了很簡單,就是在寵物出生時,先用亂數取一個數值當成這隻寵物的基因,儲存在伺服器上的同樣也只有這組基因數字。在玩家查看寵物的力量屬性,或是寵物對戰要取出力量屬性去計算傷害的時候,就將基因加上字串"力量"去作Hash,再從這個Hash裏取一段數字出來當亂數種子,最後再把亂數種子放進亂數產生器去得到這隻寵物的力量屬性。
// 決定寵物的基因
let gene = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
// 取得力量的Hash(將屬性名稱加上基因去取MD5)
let strengthHash = MD5.hash("力量" + gene);
// 從Hash裏取出最前面的八個字串(16進位的字串)
let strengthHashCut = strengthHash.substring(0, 8);
// 將切下來的字串轉換成10進位的數字
let strengthSeed = parseInt(strengthHashCut, 16);
// 建立亂數產生器
let rng = new RandomGenerator(strengthSeed);
/** 計算力量屬性(平均力量: 100,最大差異: ±10)
* 這裏的rng.next()會給出一個介於0到1的數字
* 所以(rng.next()-0.5)*2就會得到-1到1之間的亂數
*/
let strength = Math.round(100 + (rng.next() - 0.5) * 2 * 10);
console.log(`這隻寵物的力量=${strength}`);
利用這個方法,不但在伺服器上只要為每隻寵物儲存一組作為基因的整數,而且往後要擴充什麼新的屬性,比如寵物對辣椒的過敏程度或冬天待在家的進食頻率,也完全不是問題。
上面寫的程式只是先用簡單幾行解釋給同學看,下面我們正式把MD5亂數產生器寫成一個類別,這個類別會繼承前兩天寫的亂數產生器,忘記的同學可以再去回味一下。
/** 將一組數字與字串的組合,變成一個亂數種子的函式 */
function getMD5Seed(num: number, phrase: string): number {
let hash = MD5.hash(phrase + num);
let cut = hash.substring(0, 8);
return parseInt(cut, 16);
}
/** 實作MD5亂數產生器(繼承先前寫好的RandomGenerator) */
class MD5RandomGenerator extends RandomGenerator {
// 建構子
constructor(gene: number, phrase: string) {
/** 呼叫從RandomGenerator繼承來的建構子
* super()會呼叫繼承類別的建構子
* RandomGenerator的建構子需要一個亂數種子作為參數
*/
super(getMD5Seed(gene, phrase));
}
}
剛剛我們用亂數產生器去得到力量屬性時,用的是平均分配的亂數。平均力量100,最大差異±10,這樣所有寵物的的力量屬性會很平均地分布在90到110之間。
不過我們要的不是這種亂數,這樣的屬性分配太不自然。我們真正想要的是一個呈常態分布的亂數產生器,也就是說,如果黑皮熊的平均力量是100,變異數是10,那麼把遊戲中所有黑皮熊的力量放在一起看的話,絕大部分應該都要出現在100左右,只有約2%的黑皮熊力量大於120,也只有2%的力量比80還小。
以下寫一個取常態分配亂數的函式給同學們參考。
function normalDistributionRandom(
rng: RandomGenerator, // 使用這個亂數產生器
mean: number, // 平均值
dev: number, // 變異數
) {
// 平均從全圓取一個角度
let theta = rng.next() * 2 * Math.PI;
// 用指數分布取離原點距離
let dist = -Math.log(1 - rng.next());
// 用極座標取點
let point = Point.polar(dist, theta);
// 取完的點,裏面的x和y都是屬於標準常態分布的亂數
let value = point.x;
// 把標準常態分布依平均值與變異數去平移和放大
return value * dev + mean;
}
如果想深入理解如何DIY常態分布的亂數,可以參考拙作
《(寫程式玩數學#3)人工製造常態分布的亂數...不會了吧!》
我們再寫個示範程式碼來用上面寫的工具取得寵物的力量。
// 寫一個造物函式
function createPet(id: number) {
/** 定義寵物力量平均值和變異數 */
let strMean = 100;
let strDev = 10;
/** 定義寵物的基因 */
let gene = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
/** 建立MD5亂數產生器 */
let rng = new MD5RandomGenerator(gene, "力量");
/** 計算這隻寵物的力量 */
let strength = normalDistributionRandom(rng, strMean, strDev);
/** 取整數 */
strength = Math.round(strength);
console.log(`寵物(${id})的力量 = ${strength}`);
}
// 造出五隻寵物
for (let i = 1; i <= 5; i++) {
createPet(i);
}
按以上的方法,就能造出五隻寵物,他們的力量大部分會集中在100左右,但也有機會生出極少數力量達到120的精英,或甚至超越140天生神力的怪力寵。
MD5亂數產生器和常態分配的結合,很有趣吧!