#-2 Swift自定義亂數/生成隨機亂數
目錄
⦿ C語言中的rand()
⦿ 0 ~ RAND_MAX
⦿ 溢位時不取餘
⦿ Swift生成隨機亂數
⦿ arc4random()的缺陷
⦿ random、randomElement
⦿ shuffle()
C語言中的 rand( )
在 C 語言中,我們常會使用生成亂數的函式 rand( ),使用時務必要記得導入標頭檔 <stdlib.h>
。
此函數會回傳一個介於 0 ~ RAND_MAX 間的整數值。
首先生成一個變數再賦值,如下:
int a;
若要取 1 ~ 10 間的隨機亂數:a = ( rand( )%10 ) + 1
若要取 1 ~ 100 間的隨機亂數:a = ( rand( )%100) + 1
若要取 10 ~ 100 間的隨機亂數:a = ( rand( )%91 ) + 10
rand( )%10,代表 0 ~ RAND_MAX
間的隨機數對 10 取餘數,所得值為 0 ~ 9,後方再加上 1,所得值為 1 ~ 10,以此類推 rand( )%100
。
rand( )%91,代表 0 ~ RAND_MAX
間的隨機數對 91 取餘數,所得值為 0 ~ 90,後方再加上 10,所得值為 10 ~ 100。
取餘的概念以 除以2 來舉例,若不是 整除 就是 餘1,所以對 x 取餘,會得到 0 ~ x-1
。
繼續閱讀|回目錄
0 ~ RAND_MAX
由於 rand( ) 是生成 0 ~ RAND_MAX
間的隨機數,所以這個隨機數就顯得很重要了。
事實是,電腦不會真的生成隨機亂數,都是使用者下了邏輯指令,電腦依照邏輯指令而動作,我們能夠做到的『模擬』隨機。
所以即便在 C 語言中,導入了 <time.h>
後,再加上 srand( time(NULL) )
就變得足夠隨機,time 函式表示傳入時間,也就是我們說的亂數種子。因為每個人的使用時間不同,所以看起來夠亂,但實作上仍有許多問題存在。
另外,C 語言裡生成偽亂數有所謂的線性同餘法:
Xn+1 = (Xn * a + c) % m
這個式子意思是,舊數乘以一個整數再加另個整數,最後再對另個整數取餘,得到的新數。
繼續閱讀|回目錄
溢位時不取餘
在 C 語言中,我們是這樣宣告一個無號整數:
unsigned int number = 1;
若一個無號整數採用線性同餘法,乘以一個足夠大的數再加上一個數,語法上就不用再寫取餘,即是:
a很大的時候
Xn+1 = (Xn * a + c)
因為無號整數在運算時若溢位,會自動在後方除以 UINT_MAX + 1。舉例來說,如果今天 10 是你能表達出的最大數,溢位的意思則是給你 11,這時候你無法表達 11,所以你會把 11%(10+1)
,即溢位數對 最大數 + 1 取餘,得到的結果為 0 做為最終你的表達數,任意的溢位數對 最大數 + 1 取餘,會得到這樣的結果:
能表達的最大數為10,n 代表溢位數:輸出 n%11,即 0 ~ 10能表達的最大數為UINT_MAX,n 代表溢位數:輸出 n%(UINT_MAX + 1),即 0 ~ UINT_MAX繼續閱讀|回目錄
Swift生成隨機亂數
好了,回到今天的重點,Swift如何生成亂數呢?使用 SwiftUI 寫了一個撲克比大小的遊戲,整體 UI 如下:
每當贏牌,Player 或 CPU 下方分數都會加 1,就機率上來說,比的次數越多,兩方分數就會越接近。
在雙方皆使用 random( ) 的情況下,一開始用 Int.Random(in 2…14) 來寫,測試過大約一方分數到達 100 後,另一方也接近 100,如下:
2 => 2
…… 11 => J
、12 => Q
、13 => K
、14 => A
繼續閱讀|回目錄
arc4random( )的缺陷
如果撲克比大小,我們一個用 arc4random( ),一個用 random( )的話,結果會怎麼樣呢?
用 arc4random( ) % 13,表示餘下 0 ~ 12
,再加 2 表示 2 ~ 14
,跟 random(in: 2…14) 作用相同,如下:
我們按 ⌥ + 左鍵 看 arc4random( ) 的說明,會看到它的回傳值是 UInt32,其中 UInt32_MAX 即是 2³² = 4,294,967,296
。
前面 C 語言說到,宣告一個 unsigned int,溢位時會以 UINT_MAX + 1 取餘表達,即是 4,294,967,297
。
但是 arc4random( ) 的生成範圍是 0 ~ 2³² − 1 為 4,294,967,295
。
所以 4,294,967,295
% 13 = 330,382,099…8,這表示餘 1 ~ 8
的機率高了一點點,即是 3 ~ 10
的撲克牌點數出現機率也高了一點點。
所以上網查資料時,大家都說擲骰子用 arc4random( ) 不是很公平,當時的解決辦法是使用 arc4random_uniform,雖然相對公平,但轉型麻煩,這是 Swift 4.2 以前的方法,暫不討論導入 GameplayKit,或 Gauss Distribution,直接複習 Swift 4.2 後如何使用 random。
繼續閱讀|回目錄
random、randomElement
在 Swift 中使用 random(in: ) 產生隨機亂數。
使用 dot syntax,可讓 Int、Double、Float、CGFloat,甚至 Bool 都可直接呼叫 random( )。
如果是 Array,以往還要對 Array 取下標才能得到值,現在可直接使用 randomElement( ),只是 randomElement( ) 的回傳值是 optional,可能是 nil,若確定有值,需在後方加上 “!” 來 unwarp。
其他 range、dictionary、set 也都可用dot syntax
呼叫randomElement( )
。
繼續閱讀|回目錄
此外,random( ),其實還可採泛型,先看看它的定義:
接下來我們這樣用,先 enum 一個 Weekday,當它遵循 CaseIterable,裡面的 case 就可用 allCases 取下標,如 Weekday.allCases[0]
,就是 sunday
。
讓這個 enum case 變成 Array,所以這個 enum 也可用randElement( )
了。
首先,這個泛型令為 G,G 是 RandomNumberGenerator 的子類,將這個 G 傳到 random 裡做為 inout 參數,最後回傳 Weekday,即是我們要的結果。
與未採用泛型的 random( ) 相比,RandomNumberGenerator
比SystemRandomNumberGenerator
更有彈性地去生成亂數。我們會這樣使用:
var randomDay: Weekday = .random()
此時的 randomDay 就是 sunday 到 saturday 的其中一天!是不是很酷呢?
繼續閱讀|回目錄
shuffle( )
由於前面講到 Array,Swift 還提供了一個 shuffle( ) 函式,用 dot syntax,可將 Array 裡的元素重新排列,如下:
這個陣列的元素就是撲克牌的 2 ~ A,接著用 shuffle( ) 替換了他們的順序,此時再取當中的第一個元素出來比大小,也達到生成亂數的目的。
使用 random、arc4random、shuffle 這三種生成亂數的方法後,在 200 次內的較量看不太出差異,覺得都蠻公平的,莊家閒家有輸有贏。
然而,寫程式卻也不是為了公平,若是莊家一直贏大概也沒人要玩了,遊戲裡,多是讓玩家看似有機會一搏而繼續投入金錢的設計,莫怪乎當今遊戲只換皮,再導入商城、抽卡機制總能讓玩家傾瀉荷包,不再以遊戲性為依歸。
繼續閱讀|回目錄