#-2 Swift自定義亂數/生成隨機亂數

春麗 S.T.E.M.
7 min readJul 4, 2021

--

目錄
⦿ 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,如下:

SwiftUI下,兩張卡片的變化一開始便採用亂數生成

2 => 2 …… 11 => J12 => Q13 => K14 => A

繼續閱讀|回目錄

arc4random( )的缺陷

如果撲克比大小,我們一個用 arc4random( ),一個用 random( )的話,結果會怎麼樣呢?

用 arc4random( ) % 13,表示餘下 0 ~ 12 ,再加 2 表示 2 ~ 14,跟 random(in: 2…14) 作用相同,如下:

player採用arc4random ; cpu採用random

我們按 ⌥ + 左鍵 看 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( ) 相比,RandomNumberGeneratorSystemRandomNumberGenerator 更有彈性地去生成亂數。我們會這樣使用:

var randomDay: Weekday = .random()

此時的 randomDay 就是 sunday 到 saturday 的其中一天!是不是很酷呢?

繼續閱讀|回目錄

shuffle( )

由於前面講到 Array,Swift 還提供了一個 shuffle( ) 函式,用 dot syntax,可將 Array 裡的元素重新排列,如下:

struct ContentView: View

這個陣列的元素就是撲克牌的 2 ~ A,接著用 shuffle( ) 替換了他們的順序,此時再取當中的第一個元素出來比大小,也達到生成亂數的目的。

使用 random、arc4random、shuffle 這三種生成亂數的方法後,在 200 次內的較量看不太出差異,覺得都蠻公平的,莊家閒家有輸有贏。

然而,寫程式卻也不是為了公平,若是莊家一直贏大概也沒人要玩了,遊戲裡,多是讓玩家看似有機會一搏而繼續投入金錢的設計,莫怪乎當今遊戲只換皮,再導入商城、抽卡機制總能讓玩家傾瀉荷包,不再以遊戲性為依歸。

繼續閱讀|回目錄

--

--

春麗 S.T.E.M.
春麗 S.T.E.M.

Written by 春麗 S.T.E.M.

Do not go gentle into that good night, Old age should burn and rave at close of day; Rage, rage, against the dying of the light.

No responses yet