๐Ÿ“ฆ sleepyfran / duets

๐Ÿ“„ DailyUpdate.Tests.fs ยท 347 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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347module Duets.Simulation.Tests.Concerts.DailyUpdate

open NUnit.Framework
open FsCheck
open FsUnit
open Test.Common
open Test.Common.Generators

open Aether
open Duets.Entities
open Duets
open Duets.Simulation

let actAndGetConcert state =
    Concerts.DailyUpdate.dailyUpdate state
    |> fun effects ->
        match effects with
        | ConcertUpdated(_, concert) :: _ -> Concert.fromScheduled concert
        | _ -> failwith "Not possible"

let venue =
    Queries.World.placesByTypeInCity Prague PlaceTypeIndex.ConcertSpace
    |> List.find (fun place ->
        match place.PlaceType with
        | ConcertSpace venue -> venue.Capacity = 800
        | _ -> false)

let concert =
    { Id = Identity.create ()
      CityId = Prague
      VenueId = venue.Id
      Date = Calendar.gameBeginning |> Calendar.Ops.addDays 30<days>
      DayMoment = Night
      TicketPrice = 20m<dd>
      TicketsSold = 0
      ParticipationType = Headliner }

[<Test>]
let ``generates as many effects as concerts are scheduled`` () =
    State.generateN
        { State.defaultOptions with
            FutureConcertsToGenerate = 10 }
        100
    |> List.iter (fun state ->
        let effects = Concerts.DailyUpdate.dailyUpdate state

        let band = Simulation.Queries.Bands.currentBand state

        let concerts =
            Optic.get (Lenses.FromState.Concerts.allByBand_ band.Id) state
            |> Option.get

        List.length effects
        |> should equal (List.length concerts.ScheduledEvents))

[<Test>]
let ``generates sold tickets based on band's fame, venue capacity, last time visit, ticket price and days until the concert``
    ()
    =
    State.generateN
        { State.defaultOptions with
            BandFansMin = 2000<fans>
            BandFansMax = 9000<fans> }
        50
    |> List.iter (fun state ->
        let state =
            state
            |> State.Concerts.addScheduledConcert
                dummyBand
                (ScheduledConcert(concert, dummyToday))

        let concert = actAndGetConcert state
        concert.TicketsSold |> should be (lessThanOrEqualTo 50))

[<Test>]
let ``sold tickets get lower when band fame is lower`` () =
    let state =
        State.generateOne
            { State.defaultOptions with
                BandFansMin = 50<fans>
                BandFansMax = 50<fans> }
        |> State.Concerts.addScheduledConcert
            dummyBand
            (ScheduledConcert(
                { concert with
                    Date = dummyToday |> Calendar.Ops.addDays 1<days>
                    TicketPrice = 2m<dd> },
                dummyToday
            ))

    let concert = actAndGetConcert state
    concert.TicketsSold |> should equal 12

[<Test>]
let ``sold tickets get added to the previously sold tickets`` () =
    let state =
        State.generateOne
            { State.defaultOptions with
                BandFansMin = 25000<fans>
                BandFansMax = 25000<fans> }
        |> State.Concerts.addScheduledConcert
            dummyBand
            (ScheduledConcert({ concert with TicketsSold = 10 }, dummyToday))

    let concert = actAndGetConcert state
    concert.TicketsSold |> should equal 135

[<Test>]
let ``daily sold tickets are calculated based on how many days are left until the concert``
    ()
    =
    let concert =
        State.generateOne
            { State.defaultOptions with
                BandFansMin = 2500<fans>
                BandFansMax = 2500<fans> }
        |> State.Concerts.addScheduledConcert
            dummyBand
            (ScheduledConcert(
                { concert with
                    Date = dummyToday |> Calendar.Ops.addDays 15<days>
                    TicketPrice = 10m<dd> },
                dummyToday
            ))
        |> actAndGetConcert

    concert.TicketsSold |> should equal 24

let newYorkVenue =
    Queries.World.placesByTypeInCity NewYork PlaceTypeIndex.ConcertSpace
    |> List.head

[<Test>]
let ``daily sold tickets are calculated based on the fans in the concert's city``
    ()
    =
    let state =
        State.generateOne
            { State.defaultOptions with
                // These fans will be added only to the city of Prague by default.
                BandFansMin = 25000<fans>
                BandFansMax = 25000<fans> }
        |> State.Concerts.addScheduledConcert
            dummyBand
            (ScheduledConcert(
                { concert with
                    TicketsSold = 0
                    CityId = NewYork
                    VenueId = newYorkVenue.Id },
                dummyToday
            ))

    let concert = actAndGetConcert state
    concert.TicketsSold |> should equal 2

let actAndGetConcertWithPrice price =
    State.generateOne
        { State.defaultOptions with
            BandFansMin = 25000<fans>
            BandFansMax = 25000<fans> }
    |> State.Concerts.addScheduledConcert
        dummyBand
        (ScheduledConcert({ concert with TicketPrice = price }, dummyToday))
    |> actAndGetConcert

[<Test>]
let ``sold tickets decrease if ticket price gets close to the price cap`` () =
    let concert = actAndGetConcertWithPrice 23m<dd>

    concert.TicketsSold |> should equal 103

    let concert = actAndGetConcertWithPrice 24m<dd>

    concert.TicketsSold |> should equal 71

    let concert = actAndGetConcertWithPrice 25m<dd>

    concert.TicketsSold |> should equal 71

[<Test>]
let ``sold tickets decrease when price goes slightly above price cap`` () =
    let concert = actAndGetConcertWithPrice 26m<dd>

    concert.TicketsSold |> should equal 71

    let concert = actAndGetConcertWithPrice 27m<dd>

    concert.TicketsSold |> should equal 71

    let concert = actAndGetConcertWithPrice 28m<dd>

    concert.TicketsSold |> should equal 71

[<Test>]
let ``sold tickets decrease a lot when price goes over price cap`` () =
    let concert = actAndGetConcertWithPrice 30m<dd>

    concert.TicketsSold |> should equal 35

    let concert = actAndGetConcertWithPrice 35m<dd>

    concert.TicketsSold |> should equal 12

    let concert = actAndGetConcertWithPrice 60m<dd>

    concert.TicketsSold |> should equal 0


[<Test>]
let ``sold tickets are capped to venue capacity`` () =
    let concert =
        State.generateOne
            { State.defaultOptions with
                BandFansMax = 25<fans> }
        |> State.Concerts.addScheduledConcert
            dummyBand
            (ScheduledConcert({ concert with TicketsSold = 1500 }, dummyToday))
        |> actAndGetConcert

    concert.TicketsSold |> should equal 800

[<Test>]
let ``sold tickets should not decrease out of the normal cap when last visit to the city was more than 180 days ago``
    ()
    =
    let concertInCityGenerator =
        gen {
            (* The concert is scheduled in 30 days, thus the weird from and to. *)
            return!
                Concert.pastConcertGenerator
                    { Concert.defaultOptions with
                        From = dummyToday |> Calendar.Ops.addYears -2<years>
                        To = dummyToday |> Calendar.Ops.addDays -80<days> }
        }

    let concert =
        State.generateOne
            { State.defaultOptions with
                BandFansMin = 25000<fans>
                BandFansMax = 25000<fans>
                PastConcertsToGenerate = 1
                PastConcertGen = concertInCityGenerator }
        |> State.Concerts.addScheduledConcert
            dummyBand
            (ScheduledConcert(concert, dummyToday))
        |> actAndGetConcert

    concert.TicketsSold |> should equal 125

[<Test>]
let ``sold tickets decrease to 70% of the normal cap when last visit to the city was less than 30 days ago``
    ()
    =
    let concertInCityGenerator =
        gen {
            (* The concert is scheduled in 30 days, thus the weird from and to. *)
            return!
                Concert.pastConcertGenerator
                    { Concert.defaultOptions with
                        From = dummyToday |> Calendar.Ops.addDays 1<days>
                        To = dummyToday |> Calendar.Ops.addDays 15<days> }
        }

    let concert =
        State.generateOne
            { State.defaultOptions with
                BandFansMin = 250000<fans>
                BandFansMax = 250000<fans>
                PastConcertsToGenerate = 1
                PastConcertGen = concertInCityGenerator }
        |> State.Concerts.addScheduledConcert
            dummyBand
            (ScheduledConcert(concert, dummyToday))
        |> actAndGetConcert

    concert.TicketsSold |> should equal 800

[<Test>]
let ``sold tickets decrease to 20% of the normal cap when last visit to the city was less than 10 days ago``
    ()
    =
    let concertInCityGenerator =
        gen {
            (* The concert is scheduled in 30 days, thus the weird from and to. *)
            return!
                Concert.pastConcertGenerator
                    { Concert.defaultOptions with
                        From = dummyToday |> Calendar.Ops.addDays 20<days>
                        To = dummyToday |> Calendar.Ops.addDays 30<days> }
        }

    let concert =
        State.generateOne
            { State.defaultOptions with
                BandFansMin = 250000<fans>
                BandFansMax = 250000<fans>
                PastConcertsToGenerate = 5
                PastConcertGen = concertInCityGenerator }
        |> State.Concerts.addScheduledConcert
            dummyBand
            (ScheduledConcert(concert, dummyToday))
        |> actAndGetConcert

    concert.TicketsSold |> should equal 248

[<Test>]
let ``does not compute daily tickets sold as infinity when the days until the concert are 0``
    ()
    =
    let concert =
        State.generateOne
            { State.defaultOptions with
                BandFansMin = 2500000<fans>
                BandFansMax = 2500000<fans> }
        |> State.Concerts.addScheduledConcert
            dummyBand
            (ScheduledConcert({ concert with Date = dummyToday }, dummyToday))
        |> actAndGetConcert

    concert.TicketsSold |> should equal 800

[<Test>]
let ``computes daily tickets based on headliner if participation type is opening act``
    ()
    =
    let concert =
        State.generateOne
            { State.defaultOptions with
                BandFansMin = 250<fans>
                BandFansMax = 250<fans> }
        |> State.Bands.addSimulated
            { dummyHeadlinerBand with
                Fans = [ concert.CityId, 1200<fans> ] |> Map.ofList }
        |> State.Concerts.addScheduledConcert
            dummyBand
            (ScheduledConcert(
                { concert with
                    Date = dummyToday
                    TicketPrice = 6m<dd>
                    ParticipationType =
                        OpeningAct(dummyHeadlinerBand.Id, 50<percent>) },
                dummyToday
            ))
        |> actAndGetConcert

    concert.TicketsSold |> should equal 208