๐Ÿ“ฆ sleepyfran / duets

๐Ÿ“„ RandomGen.fs ยท 122 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122module Duets.Simulation.RandomGen

open Duets.Common

type GenFunc = unit -> int
type GenBetweenFunc = int -> int -> int

type RandomGenAgentMessage =
    | Change of System.Random
    | Reset
    | Gen of AsyncReplyChannel<int>
    | GenDouble of AsyncReplyChannel<double>
    | GenBetween of min: int * max: int * channel: AsyncReplyChannel<int>

/// Agent that encapsulates a random number generator to not have to pass a
/// function parameter to every part of the Simulation assembly using this
/// and allow to easy mocking during testing. An overblown solution? Maybe, but
/// it might evolve into an agent holding a DI framework if the situation
/// requires so in the future :)
type private RandomGenAgent() =
    let defaultRandom = System.Random()

    let agent =
        MailboxProcessor.Start
        <| fun inbox ->
            let rec loop random =
                async {
                    let! msg = inbox.Receive()

                    match msg with
                    | Change r -> return! loop r
                    | Reset -> return! loop defaultRandom
                    | Gen channel ->
                        random.Next() |> channel.Reply
                        return! loop random
                    | GenDouble channel ->
                        random.NextDouble() |> channel.Reply
                        return! loop random
                    | GenBetween(min, max, channel) ->
                        random.Next(min, max) |> channel.Reply
                        return! loop random
                }

            loop defaultRandom

    member this.Change genFunc = genFunc |> agent.Post
    member this.Reset() = Reset |> agent.Post
    member this.Gen() = agent.PostAndReply Gen

    member this.GenDouble() = agent.PostAndReply GenDouble

    member this.GenBetween min max =
        agent.PostAndReply(fun channel -> GenBetween(min, max, channel))

let private randomGenAgent = RandomGenAgent()

let change impl = Change impl |> randomGenAgent.Change

let reset = randomGenAgent.Reset

let gen = randomGenAgent.Gen

let genBetween = randomGenAgent.GenBetween

let genDouble = randomGenAgent.GenDouble

/// Generates a random number between 0 and 100 and returns true if it is
/// less than or equal to the given amount.
let chance amount =
    let random = genBetween 0 100
    random <= amount

let sampleIndex list =
    let maxIndex = List.length list - 1
    let maxIndex = if maxIndex < 0 then 0 else maxIndex
    genBetween 0 maxIndex

let choice choices = List.item (sampleIndex choices) choices

let tryChoice choices =
    List.tryItem (sampleIndex choices) choices

/// Distributes a total amount of items into a list of items so that
/// the sum of the items is equal to the total. The items are distributed
/// randomly.
let distribute (total: int<_>) (items: 'a list) : Map<'a, int> =
    // Generate random weights and normalize them.
    let weights = items |> List.map (fun _ -> genDouble ())
    let totalWeight = List.sum weights
    let normalizedWeights = weights |> List.map (fun w -> w / totalWeight)

    // Distribute the items based on the normalized weights.
    let distributedItems =
        normalizedWeights
        |> List.map (fun w -> float total * w |> Math.ceilToNearest)

    List.zip items distributedItems |> Map.ofList

/// Selects a random element from a list of choices, where each choice has an
/// associated weight, and the probability of selecting each choice is
/// proportional to its weight. If the total weight is zero or negative, returns None.
let weightedRandomChoice (choices: ('T * float) list) : 'T option =
    let totalWeight = choices |> List.sumBy snd

    if totalWeight <= 0.0 then
        None
    else
        let rand = genDouble () * totalWeight

        let rec pick prevCumulative =
            function
            | [] -> None
            | (value, weight) :: tail ->
                let cumulative = prevCumulative + weight

                if rand <= cumulative then
                    Some value
                else
                    pick cumulative tail

        pick 0.0 choices