From 7c2ac4a3d18e79343a79c4baf37ac140eb4e5aa0 Mon Sep 17 00:00:00 2001 From: sleepyfran Date: Sat, 19 Oct 2024 22:10:47 +0200 Subject: [PATCH 1/7] :card_file_box: Annotate cities with their country --- src/Duets.Data/World/World.fs | 15 +++++++++++++++ src/Duets.Entities/Types/City.Types.fs | 11 +++++++++++ tests/Data.Tests/World.Tests.fs | 10 ++++++++++ 3 files changed, 36 insertions(+) diff --git a/src/Duets.Data/World/World.fs b/src/Duets.Data/World/World.fs index 3ba6709e..3a174097 100644 --- a/src/Duets.Data/World/World.fs +++ b/src/Duets.Data/World/World.fs @@ -28,6 +28,21 @@ let private generate () = /// Returns the game world. The world is initialized when the module is loaded. let get = generate () +/// Defines the metadata about the country a certain city belongs to. +let private countryMetadata: Map = + [ (London, England) + (LosAngeles, UnitedStates) + (Madrid, Spain) + (MexicoCity, Mexico) + (NewYork, UnitedStates) + (Prague, CzechRepublic) + (Sydney, Australia) + (Tokyo, Japan) ] + |> Map.ofList + +/// Returns the country of the given city. +let countryOf city = countryMetadata |> Map.find city + /// Defines different metadata about the connections between cities: the /// distance between them and which connections are available (road, sea or air) let private connectionMetadata diff --git a/src/Duets.Entities/Types/City.Types.fs b/src/Duets.Entities/Types/City.Types.fs index b65e5b19..1c95e55d 100644 --- a/src/Duets.Entities/Types/City.Types.fs +++ b/src/Duets.Entities/Types/City.Types.fs @@ -2,6 +2,17 @@ namespace Duets.Entities [] module CityTypes = + /// ID for a country in the game world, which declared every possible country + /// available in the game. + type CountryId = + | Australia + | CzechRepublic + | England + | Japan + | Mexico + | Spain + | UnitedStates + /// ID for a city in the game world, which declared every possible city /// available in the game. type CityId = diff --git a/tests/Data.Tests/World.Tests.fs b/tests/Data.Tests/World.Tests.fs index fbd5c673..421e583b 100644 --- a/tests/Data.Tests/World.Tests.fs +++ b/tests/Data.Tests/World.Tests.fs @@ -36,6 +36,16 @@ let ``all city IDs are added to the world`` () = let ``all cities are connected to each other`` () = World.get.Cities |> List.ofMapValues |> checkCities +[] +let ``all cities have a country`` () = + World.get.Cities + |> List.ofMapValues + |> List.iter (fun city -> + (fun () -> World.countryOf city.Id |> ignore) + |> should + not' + (throw typeof)) + let private checkAtLeastOneWithCapacity cityId concertSpaces From 1011e41798fb62260332103d49b3badb39482a84 Mon Sep 17 00:00:00 2001 From: sleepyfran Date: Mon, 21 Oct 2024 11:22:08 +0200 Subject: [PATCH 2/7] Switch to region-base, so code, much wow --- .../Commands/Cheats/Band.Cheats.Commands.fs | 18 +++++++- .../ScheduleOpeningActShow.fs | 4 +- .../Phone/Apps/Statistics/BandStatistics.fs | 12 +++-- src/Duets.Entities/Band.fs | 4 +- src/Duets.Entities/Types/Band.Types.fs | 8 +++- src/Duets.Entities/Types/Effect.Types.fs | 2 +- src/Duets.Simulation/Albums/DailyStreams.fs | 5 +- src/Duets.Simulation/Albums/DailyUpdate.fs | 9 ++-- src/Duets.Simulation/Albums/FanIncrease.fs | 6 +++ .../Albums/ReviewGeneration.fs | 7 ++- src/Duets.Simulation/Bands/Generation.fs | 17 +++++-- src/Duets.Simulation/Concerts/DailyUpdate.fs | 4 +- .../Concerts/Live/Live.Finish.fs | 28 ++++++----- .../Events/Band/Band.Events.fs | 26 +++++++---- src/Duets.Simulation/Queries/Bands.fs | 29 +++++++++--- src/Duets.Simulation/Queries/Concerts.fs | 11 +++-- .../SocialNetworks/DailyUpdate.fs | 5 +- src/Duets.Simulation/State/Bands.fs | 2 +- .../Albums/DailyUpdate.Tests.fs | 17 ++++--- .../Albums/ReviewGeneration.Tests.fs | 35 ++++++++------ .../Bands/BandGeneration.Tests.fs | 20 ++++++-- .../Concerts/DailyUpdate.Tests.fs | 46 ++++++++++--------- .../Concerts/Live.Finish.Tests.fs | 15 ++++-- .../Concerts/OpeningActOpportunities.Tests.fs | 40 ++++++++-------- .../Events/Band.Events.Tests.fs | 7 ++- .../Events/Character.Events.Tests.fs | 6 ++- .../SocialNetworks/DailyUpdate.Tests.fs | 6 +-- .../Test.Common/Generators/Band.Generator.fs | 13 ------ .../Test.Common/Generators/State.Generator.fs | 15 +++--- tests/Test.Common/Library.fs | 2 +- tests/Test.Common/Test.Common.fsproj | 1 - 31 files changed, 261 insertions(+), 159 deletions(-) delete mode 100644 tests/Test.Common/Generators/Band.Generator.fs diff --git a/src/Duets.Cli/Components/Commands/Cheats/Band.Cheats.Commands.fs b/src/Duets.Cli/Components/Commands/Cheats/Band.Cheats.Commands.fs index 6938062a..b12f91d6 100644 --- a/src/Duets.Cli/Components/Commands/Cheats/Band.Cheats.Commands.fs +++ b/src/Duets.Cli/Components/Commands/Cheats/Band.Cheats.Commands.fs @@ -20,11 +20,25 @@ module BandCommands = (fun _ -> let band = Queries.Bands.currentBand (State.get ()) + let allCities = Queries.World.allCities + + let chosenCity = + showChoicePrompt + "Where do you want to change your fans?" + (fun (city: City) -> Generic.cityName city.Id) + allCities + + let fansInCity = Queries.Bands.fansInCity' band chosenCity.Id + let fans = - $"You currently have {band.Fans}, how many fans do you want?" + $"You currently have {fansInCity}, how many fans do you want there?" |> Styles.prompt |> showNumberPrompt + |> (*) 1 + + let updatedFans = band.Fans |> Map.add chosenCity.Id fans - BandFansChanged(band, Diff(band.Fans, fans)) |> Effect.apply + BandFansChanged(band, Diff(band.Fans, updatedFans)) + |> Effect.apply Scene.Cheats) } diff --git a/src/Duets.Cli/Scenes/Phone/Apps/ConcertAssistant/ScheduleOpeningActShow.fs b/src/Duets.Cli/Scenes/Phone/Apps/ConcertAssistant/ScheduleOpeningActShow.fs index 5bf10268..293a4e85 100644 --- a/src/Duets.Cli/Scenes/Phone/Apps/ConcertAssistant/ScheduleOpeningActShow.fs +++ b/src/Duets.Cli/Scenes/Phone/Apps/ConcertAssistant/ScheduleOpeningActShow.fs @@ -53,7 +53,9 @@ let private promptForConcert app allPotentialConcerts date concertsOnDate = | PlaceType.ConcertSpace space -> space.Capacity | _ -> 0 - $"Headliner {Styles.highlight band.Name} ({band.Genre}/{band.Fans |> Styles.number} fans) @ {Styles.place place.Name} ({capacity} capacity)") + let fansInCity = Queries.Bands.fansInCity' band concert.CityId + + $"Headliner {Styles.highlight band.Name} ({band.Genre}/{fansInCity |> Styles.number} fans) @ {Styles.place place.Name} ({capacity} capacity)") concertsOnDate match selectedConcert with diff --git a/src/Duets.Cli/Scenes/Phone/Apps/Statistics/BandStatistics.fs b/src/Duets.Cli/Scenes/Phone/Apps/Statistics/BandStatistics.fs index c5822276..e834303b 100644 --- a/src/Duets.Cli/Scenes/Phone/Apps/Statistics/BandStatistics.fs +++ b/src/Duets.Cli/Scenes/Phone/Apps/Statistics/BandStatistics.fs @@ -3,24 +3,26 @@ module Duets.Cli.Scenes.Phone.Apps.Statistics.Band open Duets.Agents open Duets.Cli.Components open Duets.Cli.Text -open Duets.Simulation.Queries +open Duets.Simulation let bandStatisticsSubScene statisticsApp = let state = State.get () - let band = Bands.currentBand state - let estimatedFame = Bands.estimatedFameLevel state band.Id + let band = Queries.Bands.currentBand state + let estimatedFame = Queries.Bands.estimatedFameLevel state band.Id let tableColumns = [ Styles.header "Name" Styles.header "Genre" Styles.header "Start Date" - Styles.header "Fans" ] + Styles.header "Fans around the world" ] + + let totalFans = Queries.Bands.totalFans' band let tableRows = [ Styles.title band.Name band.Genre Styles.highlight band.StartDate.Year - $"""{Styles.number band.Fans} ({estimatedFame |> Styles.Level.from}%% fame)""" ] + $"""{Styles.number totalFans} ({estimatedFame |> Styles.Level.from}%% fame)""" ] showTable tableColumns [ tableRows ] diff --git a/src/Duets.Entities/Band.fs b/src/Duets.Entities/Band.fs index 40bb228f..6b50e7a4 100644 --- a/src/Duets.Entities/Band.fs +++ b/src/Duets.Entities/Band.fs @@ -14,7 +14,7 @@ let empty = OriginCity = NewYork Name = "" Genre = "" - Fans = 0 + Fans = Map.empty Members = [] PastMembers = [] } @@ -25,7 +25,7 @@ let from name genre initialMember startDate originCity = OriginCity = originCity Name = name Genre = genre - Fans = 0 + Fans = Map.empty Members = [ initialMember ] PastMembers = [] } diff --git a/src/Duets.Entities/Types/Band.Types.fs b/src/Duets.Entities/Types/Band.Types.fs index 6d24ca37..99d94755 100644 --- a/src/Duets.Entities/Types/Band.Types.fs +++ b/src/Duets.Entities/Types/Band.Types.fs @@ -25,7 +25,11 @@ module BandTypes = Period: Period } /// Number of fans that a band has. - type Fans = int + [] + type fans + + /// Represents the fanbase of a band by city. + type FanBaseByCity = Map> /// Represents any band inside the game, be it one that is controlled by the /// player or the ones that are created automatically to fill the game world. @@ -35,7 +39,7 @@ module BandTypes = OriginCity: CityId Name: string Genre: Genre - Fans: Fans + Fans: FanBaseByCity Members: CurrentMember list PastMembers: PastMember list } diff --git a/src/Duets.Entities/Types/Effect.Types.fs b/src/Duets.Entities/Types/Effect.Types.fs index b750e68e..1d907611 100644 --- a/src/Duets.Entities/Types/Effect.Types.fs +++ b/src/Duets.Entities/Types/Effect.Types.fs @@ -16,7 +16,7 @@ module EffectTypes = | AlbumUpdated of Band * UnreleasedAlbum | Ate of item: Item * food: EdibleItem | BalanceUpdated of BankAccountHolder * Diff - | BandFansChanged of Band * Diff + | BandFansChanged of Band * Diff | BandSwitchedGenre of Band * Diff | BookRead of Item * Book | CareerAccept of CharacterId * Job diff --git a/src/Duets.Simulation/Albums/DailyStreams.fs b/src/Duets.Simulation/Albums/DailyStreams.fs index 81b08a0a..381cc22a 100644 --- a/src/Duets.Simulation/Albums/DailyStreams.fs +++ b/src/Duets.Simulation/Albums/DailyStreams.fs @@ -5,8 +5,9 @@ open Duets.Entities open Duets.Simulation let private calculateFanStreams band = - (float band.Fans * Config.MusicSimulation.fanStreamingPercentage) - |> Math.ceil + let totalFans = Queries.Bands.totalFans' band |> float + + (totalFans * Config.MusicSimulation.fanStreamingPercentage) |> Math.ceil let private calculateNonFanStreams state (band: Band) album genreMarket = let albumQuality = Queries.Albums.quality album |> float diff --git a/src/Duets.Simulation/Albums/DailyUpdate.fs b/src/Duets.Simulation/Albums/DailyUpdate.fs index 139d7537..0b523a3e 100644 --- a/src/Duets.Simulation/Albums/DailyUpdate.fs +++ b/src/Duets.Simulation/Albums/DailyUpdate.fs @@ -27,6 +27,7 @@ let private bandDailyUpdate state bandId albumsByBand = let recalculatedHype = reduceDailyHype album let fanIncrease = calculateFanIncrease previousDayNonFanStreams + let updatedFanBase = applyFanIncrease fanIncrease band.Fans [ yield AlbumReleasedUpdate( @@ -37,12 +38,8 @@ let private bandDailyUpdate state bandId albumsByBand = if dailyRevenue > 0m
then yield (income state bandAccount dailyRevenue) - if fanIncrease > 0 then - yield - BandFansChanged( - band, - Diff(band.Fans, band.Fans + fanIncrease) - ) ]) + if fanIncrease > 0 then + yield BandFansChanged(band, Diff(band.Fans, updatedFanBase)) ]) |> List.concat /// Performs the daily update of albums from all bands. This generates the diff --git a/src/Duets.Simulation/Albums/FanIncrease.fs b/src/Duets.Simulation/Albums/FanIncrease.fs index 48e7cd4c..d6068a66 100644 --- a/src/Duets.Simulation/Albums/FanIncrease.fs +++ b/src/Duets.Simulation/Albums/FanIncrease.fs @@ -1,5 +1,6 @@ module Duets.Simulation.Albums.FanIncrease +open Duets.Entities open Duets.Common open Duets.Simulation @@ -8,3 +9,8 @@ open Duets.Simulation let calculateFanIncrease nonFanStreams = float nonFanStreams * Config.MusicSimulation.fanIncreasePercentage |> Math.ceilToNearest + |> (*) 1 + +/// Applies the given fan increase to all cities in the fan base. +let applyFanIncrease fanIncrease (currentFans: FanBaseByCity) = + currentFans |> Map.map (fun _ fans -> fans + fanIncrease) diff --git a/src/Duets.Simulation/Albums/ReviewGeneration.fs b/src/Duets.Simulation/Albums/ReviewGeneration.fs index f0d29594..f7eab174 100644 --- a/src/Duets.Simulation/Albums/ReviewGeneration.fs +++ b/src/Duets.Simulation/Albums/ReviewGeneration.fs @@ -60,9 +60,12 @@ let private generateReviewsForAlbum (band: Band) releasedAlbum = let private generateReviewsForBandAlbums state bandId albums = let band = Queries.Bands.byId state bandId - let fanBase = band.Fans + let fanBase = Queries.Bands.totalFans' band - if fanBase >= Config.MusicSimulation.minimumFanBaseForReviews then + let minimumFanBaseForReviews = + Config.MusicSimulation.minimumFanBaseForReviews * 1 + + if fanBase >= minimumFanBaseForReviews then albums |> List.fold (fun acc album -> diff --git a/src/Duets.Simulation/Bands/Generation.fs b/src/Duets.Simulation/Bands/Generation.fs index a5123c99..d9f47ca2 100644 --- a/src/Duets.Simulation/Bands/Generation.fs +++ b/src/Duets.Simulation/Bands/Generation.fs @@ -35,11 +35,11 @@ module Name = |> String.titleCase module Fans = - /// Generates a random number of fans given the fan level. + /// Generates a random number of fans given the fan level for a given city. /// - For low fame level, between 0.01 and 0.1 of the genre's market /// - For medium fame level, between 0.1 and 0.6 of the genre's market /// - For high fame level, between 0.6 and 1.0 of the genre's market - let generate state fameLevel genre = + let generateForCity state fameLevel genre cityId = let marketCap = match fameLevel with | Low -> 0.0001, 0.001 @@ -53,7 +53,18 @@ module Fans = let upperBound = usefulMarket * (fst marketCap) let lowerBound = usefulMarket * (snd marketCap) - RandomGen.choice [ upperBound; lowerBound ] |> Math.ceilToNearest + let fame = + RandomGen.choice [ upperBound; lowerBound ] + |> Math.ceilToNearest + |> (*) 1 + + cityId, fame + + /// Generates a random number of fans given the fan level for all cities + /// in the game world. + let generate state fameLevel genre : FanBaseByCity = + // TODO: Distribute fans among cities. + [ generateForCity state fameLevel genre Prague ] |> Map.ofList module Members = /// Generate a list of members of a band based on the usual roles that a diff --git a/src/Duets.Simulation/Concerts/DailyUpdate.fs b/src/Duets.Simulation/Concerts/DailyUpdate.fs index 509ef27a..d1911f45 100644 --- a/src/Duets.Simulation/Concerts/DailyUpdate.fs +++ b/src/Duets.Simulation/Concerts/DailyUpdate.fs @@ -96,8 +96,10 @@ let private concertDailyUpdate state scheduledConcert = let lastVisitModifier = lastVisitModifier state band concert + let totalFans = Queries.Bands.totalFans' band |> float + let fanAttendanceCap = - float band.Fans * 0.15 * ticketPriceModifier * lastVisitModifier + totalFans * 0.15 * ticketPriceModifier * lastVisitModifier let nonFansAttendanceCap = calculateNonFansAttendanceCap diff --git a/src/Duets.Simulation/Concerts/Live/Live.Finish.fs b/src/Duets.Simulation/Concerts/Live/Live.Finish.fs index 153677c1..1766a8ea 100644 --- a/src/Duets.Simulation/Concerts/Live/Live.Finish.fs +++ b/src/Duets.Simulation/Concerts/Live/Live.Finish.fs @@ -1,5 +1,6 @@ module Duets.Simulation.Concerts.Live.Finish +open Aether open Duets.Common open Duets.Entities open Duets.Entities.SituationTypes @@ -16,19 +17,18 @@ let private calculateFanGain ongoingConcert = let qualityFactor = match ongoingConcert.Points with | p when p <= 40 -> - float ongoingConcert.Concert.TicketsSold - * Config.MusicSimulation.concertLowPointFanDecreaseRate + Config.MusicSimulation.concertLowPointFanDecreaseRate | p when p <= 65 -> - float ongoingConcert.Concert.TicketsSold - * Config.MusicSimulation.concertMediumPointFanIncreaseRate + Config.MusicSimulation.concertMediumPointFanIncreaseRate | p when p <= 85 -> - float ongoingConcert.Concert.TicketsSold - * Config.MusicSimulation.concertGoodPointFanIncreaseRate - | _ -> - float ongoingConcert.Concert.TicketsSold - * Config.MusicSimulation.concertHighPointFanIncreaseRate + Config.MusicSimulation.concertGoodPointFanIncreaseRate + | _ -> Config.MusicSimulation.concertHighPointFanIncreaseRate - qualityFactor * participationFactor |> Math.ceilToNearest + float ongoingConcert.Concert.TicketsSold + * qualityFactor + * participationFactor + |> Math.ceilToNearest + |> (*) 1 let private calculateEarnings ongoingConcert = let earningPercentage = @@ -58,9 +58,13 @@ let private calculateEarnings ongoingConcert = /// the band and stops them from being able to perform in the venue for the day. let finishConcert state ongoingConcert = let band = Queries.Bands.currentBand state + let concertCity = ongoingConcert.Concert.CityId + let fansInCity = Queries.Bands.fansInCity' band concertCity - let updatedFans = - calculateFanGain ongoingConcert |> (+) band.Fans |> Math.lowerClamp 0 + let updatedFansInCity = + calculateFanGain ongoingConcert + fansInCity |> Math.lowerClamp 0 + + let updatedFans = Map.add concertCity updatedFansInCity band.Fans let bandAccount = Band band.Id let concertEarnings = calculateEarnings ongoingConcert diff --git a/src/Duets.Simulation/Events/Band/Band.Events.fs b/src/Duets.Simulation/Events/Band/Band.Events.fs index c2eb5ea5..41e08992 100644 --- a/src/Duets.Simulation/Events/Band/Band.Events.fs +++ b/src/Duets.Simulation/Events/Band/Band.Events.fs @@ -1,4 +1,4 @@ -module Duets.Simulation.Events.Band.Band +module rec Duets.Simulation.Events.Band.Band open Duets.Entities open Duets.Simulation @@ -8,13 +8,23 @@ open Duets.Simulation.Events /// changes, the engine might generate new reviews for their albums. let internal run effect = match effect with - | BandFansChanged(band, Diff(prevFans, currentFans)) when - prevFans < Config.MusicSimulation.minimumFanBaseForReviews - && currentFans >= Config.MusicSimulation.minimumFanBaseForReviews - -> - [ Reviews.generateReviewsAfterFanIncrease band.Id ] - |> ContinueChain - |> Some + | BandFansChanged(band, Diff(prevFans, currentFans)) -> + let previousFans = Queries.Bands.totalFans prevFans + let currentFans = Queries.Bands.totalFans currentFans + + let minimumFanBaseForReviews = + Config.MusicSimulation.minimumFanBaseForReviews * 1 + + let hasEnoughFansForReviews = + previousFans < minimumFanBaseForReviews + && currentFans >= minimumFanBaseForReviews + + if hasEnoughFansForReviews then + [ Reviews.generateReviewsAfterFanIncrease band.Id ] + |> ContinueChain + |> Some + else + None | MemberHired(_, character, _, _) -> [ Relationships.addWithMember character ] |> ContinueChain |> Some | MemberFired(_, bandMember, _) -> diff --git a/src/Duets.Simulation/Queries/Bands.fs b/src/Duets.Simulation/Queries/Bands.fs index 2de4ceff..3bfc269d 100644 --- a/src/Duets.Simulation/Queries/Bands.fs +++ b/src/Duets.Simulation/Queries/Bands.fs @@ -75,6 +75,20 @@ module Bands = |> List.averageBy (fun character -> Characters.ageOf state character |> float) + /// Returns the sum of all fans in the given fan base. + let totalFans fanBase = fanBase |> List.ofMapValues |> List.sum + + /// Returns the total amount of fans of the band across all cities. + let totalFans' band = band.Fans |> totalFans + + /// Returns the total amount of fans of the band in the given city. + let fansInCity (fanBase: FanBaseByCity) cityId = + let lens = Map.key_ cityId + Optic.get lens fanBase |> Option.defaultValue 0 + + /// Returns the total amount of fans of the band in the given city. + let fansInCity' band cityId = fansInCity band.Fans cityId + /// Gives an estimate of the band's fame between 0 and 100 based on the /// total amount of people willing to listen to the band's genre. let estimatedFameLevel state (bandId: BandId) = @@ -83,15 +97,16 @@ module Bands = let normalizedMarketSize = Genres.usefulMarketOf state band.Genre |> System.Math.Log10 - let normalizedFans = System.Math.Log10(band.Fans) + let totalFans = totalFans' band + let normalizedFans = System.Math.Log10(totalFans |> double) let fameScalingFactor = - match band.Fans with - | fans when fans < 500 -> 8.0 - | fans when fans < 1000 -> 4.0 - | fans when fans < 10000 -> 2.5 - | fans when fans < 100000 -> 2.0 - | fans when fans < 800000 -> 1.5 + match totalFans with + | fans when fans < 500 -> 8.0 + | fans when fans < 1000 -> 4.0 + | fans when fans < 10000 -> 2.5 + | fans when fans < 100000 -> 2.0 + | fans when fans < 800000 -> 1.5 | _ -> 1 (normalizedFans / (fameScalingFactor * normalizedMarketSize)) * 100.0 diff --git a/src/Duets.Simulation/Queries/Concerts.fs b/src/Duets.Simulation/Queries/Concerts.fs index 7ebb7e29..289be931 100644 --- a/src/Duets.Simulation/Queries/Concerts.fs +++ b/src/Duets.Simulation/Queries/Concerts.fs @@ -139,12 +139,13 @@ let fairTicketPrice state bandId = /// for the given band. let suitableVenueCapacity state bandId = let band = Bands.byId state bandId + let totalFans = Bands.totalFans' band - match band.Fans with - | fans when fans <= 1000 -> (0, 300) - | fans when fans <= 5000 -> (0, 500) - | fans when fans <= 20000 -> (500, 5000) - | fans when fans <= 100000 -> (500, 20000) + match totalFans with + | fans when fans <= 1000 -> (0, 300) + | fans when fans <= 5000 -> (0, 500) + | fans when fans <= 20000 -> (500, 5000) + | fans when fans <= 100000 -> (500, 20000) | _ -> (500, 500000) /// Calculates the percentage off the tickets that the concert space will take diff --git a/src/Duets.Simulation/SocialNetworks/DailyUpdate.fs b/src/Duets.Simulation/SocialNetworks/DailyUpdate.fs index c599d424..40605235 100644 --- a/src/Duets.Simulation/SocialNetworks/DailyUpdate.fs +++ b/src/Duets.Simulation/SocialNetworks/DailyUpdate.fs @@ -7,10 +7,11 @@ open Duets.Simulation module DailyUpdate = let private estimatedFollowersNeeded state (account: SocialNetworkAccount) = let band = Queries.Bands.currentBand state + let totalFans = Queries.Bands.totalFans' band |> float match account.Id with - | SocialNetworkAccountId.Character _ -> float band.Fans * 0.4 - | SocialNetworkAccountId.Band _ -> float band.Fans * 0.7 + | SocialNetworkAccountId.Character _ -> totalFans * 0.4 + | SocialNetworkAccountId.Band _ -> totalFans * 0.7 |> Math.ceilToNearest let private increaseFollowers accountId current needed = diff --git a/src/Duets.Simulation/State/Bands.fs b/src/Duets.Simulation/State/Bands.fs index 6b3956f7..370f3c08 100644 --- a/src/Duets.Simulation/State/Bands.fs +++ b/src/Duets.Simulation/State/Bands.fs @@ -56,7 +56,7 @@ let removeMember (band: Band) (currentMember: CurrentMember) state = Optic.map membersLens removeMember state -let changeFans (band: Band) (fans: int) state = +let changeFans (band: Band) fans state = let fansLens = createBandLens band state >?> Lenses.Band.fans_ Optic.set fansLens fans state diff --git a/tests/Simulation.Tests/Albums/DailyUpdate.Tests.fs b/tests/Simulation.Tests/Albums/DailyUpdate.Tests.fs index 42fecf71..4896a3a6 100644 --- a/tests/Simulation.Tests/Albums/DailyUpdate.Tests.fs +++ b/tests/Simulation.Tests/Albums/DailyUpdate.Tests.fs @@ -16,8 +16,8 @@ let album = Album.Released.fromUnreleased dummyUnreleasedAlbum dummyToday 1.0 let state = State.generateOne { State.defaultOptions with - BandFansMin = 100 - BandFansMax = 1000 } + BandFansMin = 100 + BandFansMax = 1000 } |> addReleasedAlbum dummyBand.Id album [] @@ -77,14 +77,16 @@ let private testDailyUpdateWith minFans maxFans maxFanDifference = |> Option.orElse (List.tryItem 1 updateEffects) |> Option.get - let (Diff (_, updatedFanCount)) = + let (Diff(_, updatedFanCount)) = match effect with - | BandFansChanged (_, diff) -> diff + | BandFansChanged(_, diff) -> diff | _ -> failwith "That's not the type we were expecting" let band = Queries.Bands.currentBand state + let totalUpdatedFanCount = Queries.Bands.totalFans updatedFanCount + let previousTotalFanCount = Queries.Bands.totalFans' band - updatedFanCount - band.Fans + totalUpdatedFanCount - previousTotalFanCount |> should be (lessThanOrEqualTo maxFanDifference)) [] @@ -95,4 +97,7 @@ let ``dailyUpdate should increase fans based on streams`` () = (250001, 1500000, 1450) (1500001, 10000000, 1500) ] |> List.iter (fun (minFans, maxFans, maxExpectedDifference) -> - testDailyUpdateWith minFans maxFans maxExpectedDifference) + testDailyUpdateWith + (minFans * 1) + (maxFans * 1) + maxExpectedDifference) diff --git a/tests/Simulation.Tests/Albums/ReviewGeneration.Tests.fs b/tests/Simulation.Tests/Albums/ReviewGeneration.Tests.fs index 1f11a1fc..fc3e8412 100644 --- a/tests/Simulation.Tests/Albums/ReviewGeneration.Tests.fs +++ b/tests/Simulation.Tests/Albums/ReviewGeneration.Tests.fs @@ -52,8 +52,9 @@ let ``generateReviews should return empty if band has not released any albums`` = State.generateOne { State.defaultOptions with - BandFansMin = Config.MusicSimulation.minimumFanBaseForReviews - BandFansMax = 10000 } + BandFansMin = + Config.MusicSimulation.minimumFanBaseForReviews * 1 + BandFansMax = 10000 } |> generateReviewsForLatestAlbums |> should haveLength 0 @@ -63,8 +64,9 @@ let ``generateReviews should return empty if band does not have the minimum requ = State.generateN { State.defaultOptions with - BandFansMin = 0 - BandFansMax = Config.MusicSimulation.minimumFanBaseForReviews } + BandFansMin = 0 + BandFansMax = + Config.MusicSimulation.minimumFanBaseForReviews * 1 } 50 |> List.iter (fun state -> state @@ -80,8 +82,9 @@ let ``generateReviews should return empty if band does not have any albums relea |> List.iter (fun days -> State.generateOne { State.defaultOptions with - BandFansMin = Config.MusicSimulation.minimumFanBaseForReviews - BandFansMax = 10000 } + BandFansMin = + Config.MusicSimulation.minimumFanBaseForReviews * 1 + BandFansMax = 10000 } |> addAlbumReleasedDaysAgo days |> generateReviewsForLatestAlbums |> should haveLength 0) @@ -92,8 +95,9 @@ let ``generateReviews should return empty if the band's albums already have revi = State.generateN { State.defaultOptions with - BandFansMin = Config.MusicSimulation.minimumFanBaseForReviews - BandFansMax = 10000 } + BandFansMin = + Config.MusicSimulation.minimumFanBaseForReviews * 1 + BandFansMax = 10000 } 50 |> List.iter (fun state -> state @@ -114,8 +118,9 @@ let ``generateReviews should return effects if the day was three days ago regard State.generateOne { State.defaultOptions with - BandFansMin = Config.MusicSimulation.minimumFanBaseForReviews - BandFansMax = 10000 } + BandFansMin = + Config.MusicSimulation.minimumFanBaseForReviews * 1 + BandFansMax = 10000 } |> addReleasedAlbum dummyBand.Id { album with ReleaseDate = releaseDate } @@ -128,8 +133,9 @@ let ``generateReviews should return effects for each album released three days a = State.generateOne { State.defaultOptions with - BandFansMin = Config.MusicSimulation.minimumFanBaseForReviews - BandFansMax = 10000 } + BandFansMin = + Config.MusicSimulation.minimumFanBaseForReviews * 1 + BandFansMax = 10000 } |> addAlbumWithNoReviews |> generateReviewsForLatestAlbums |> should haveLength 1 @@ -138,8 +144,9 @@ let private testReviewScore reviewerId assertFn quality = let effects = State.generateOne { State.defaultOptions with - BandFansMin = Config.MusicSimulation.minimumFanBaseForReviews - BandFansMax = 10000 } + BandFansMin = + Config.MusicSimulation.minimumFanBaseForReviews * 1 + BandFansMax = 10000 } |> addAlbumWithQuality quality |> generateReviewsForLatestAlbums diff --git a/tests/Simulation.Tests/Bands/BandGeneration.Tests.fs b/tests/Simulation.Tests/Bands/BandGeneration.Tests.fs index 28bb8b96..c08802e2 100644 --- a/tests/Simulation.Tests/Bands/BandGeneration.Tests.fs +++ b/tests/Simulation.Tests/Bands/BandGeneration.Tests.fs @@ -34,29 +34,39 @@ type ``addInitialBandsToState``() = [] member _.``should have 50 bands of low fame level``() = simulatedBands - |> Map.filter (fun _ band -> band.Fans >=< (400, 4000)) + |> Map.filter (fun _ band -> + let totalFans = Queries.Bands.totalFans' band + totalFans >=< (400, 4000)) |> should haveCount 50 [] member _.``should generate 50 bands of low-medium fame level``() = simulatedBands - |> Map.filter (fun _ band -> band.Fans >=< (4400, 40000)) + |> Map.filter (fun _ band -> + let totalFans = Queries.Bands.totalFans' band + totalFans >=< (4400, 40000)) |> should haveCount 50 [] member _.``should generate 25 bands of medium fame level``() = simulatedBands - |> Map.filter (fun _ band -> band.Fans >=< (40001, 400000)) + |> Map.filter (fun _ band -> + let totalFans = Queries.Bands.totalFans' band + totalFans >=< (40001, 400000)) |> should haveCount 25 [] member _.``should generate 15 bands of medium-high fame level``() = simulatedBands - |> Map.filter (fun _ band -> band.Fans >=< (400000, 2000000)) + |> Map.filter (fun _ band -> + let totalFans = Queries.Bands.totalFans' band + totalFans >=< (400000, 2000000)) |> should haveCount 15 [] member _.``should generate 10 bands of high fame level``() = simulatedBands - |> Map.filter (fun _ band -> band.Fans >=< (2000000, 4000000)) + |> Map.filter (fun _ band -> + let totalFans = Queries.Bands.totalFans' band + totalFans >=< (2000000, 4000000)) |> should haveCount 10 diff --git a/tests/Simulation.Tests/Concerts/DailyUpdate.Tests.fs b/tests/Simulation.Tests/Concerts/DailyUpdate.Tests.fs index 62b20111..4cbcae00 100644 --- a/tests/Simulation.Tests/Concerts/DailyUpdate.Tests.fs +++ b/tests/Simulation.Tests/Concerts/DailyUpdate.Tests.fs @@ -42,8 +42,8 @@ let ``generates sold tickets based on band's fame, venue capacity, last time vis = State.generateN { State.defaultOptions with - BandFansMin = 2000 - BandFansMax = 9000 } + BandFansMin = 2000 + BandFansMax = 9000 } 50 |> List.iter (fun state -> let state = @@ -60,8 +60,8 @@ let ``sold tickets get lower when band fame is lower`` () = let state = State.generateOne { State.defaultOptions with - BandFansMin = 50 - BandFansMax = 50 } + BandFansMin = 50 + BandFansMax = 50 } |> State.Concerts.addScheduledConcert dummyBand (ScheduledConcert( @@ -79,8 +79,8 @@ let ``sold tickets get added to the previously sold tickets`` () = let state = State.generateOne { State.defaultOptions with - BandFansMin = 25000 - BandFansMax = 25000 } + BandFansMin = 25000 + BandFansMax = 25000 } |> State.Concerts.addScheduledConcert dummyBand (ScheduledConcert( @@ -98,8 +98,8 @@ let ``daily sold tickets are calculated based on how many days are left until th let concert = State.generateOne { State.defaultOptions with - BandFansMin = 2500 - BandFansMax = 2500 } + BandFansMin = 2500 + BandFansMax = 2500 } |> State.Concerts.addScheduledConcert dummyBand (ScheduledConcert( @@ -115,8 +115,8 @@ let ``daily sold tickets are calculated based on how many days are left until th let actAndGetConcertWithPrice price = State.generateOne { State.defaultOptions with - BandFansMin = 25000 - BandFansMax = 25000 } + BandFansMin = 25000 + BandFansMax = 25000 } |> State.Concerts.addScheduledConcert dummyBand (ScheduledConcert( @@ -174,7 +174,7 @@ let ``sold tickets are capped to venue capacity`` () = let concert = State.generateOne { State.defaultOptions with - BandFansMax = 25 } + BandFansMax = 25 } |> State.Concerts.addScheduledConcert dummyBand (ScheduledConcert( @@ -202,8 +202,8 @@ let ``sold tickets should not decrease out of the normal cap when last visit to let concert = State.generateOne { State.defaultOptions with - BandFansMin = 25000 - BandFansMax = 25000 + BandFansMin = 25000 + BandFansMax = 25000 PastConcertsToGenerate = 1 PastConcertGen = concertInCityGenerator } |> State.Concerts.addScheduledConcert @@ -230,8 +230,8 @@ let ``sold tickets decrease to 70% of the normal cap when last visit to the city let concert = State.generateOne { State.defaultOptions with - BandFansMin = 250000 - BandFansMax = 250000 + BandFansMin = 250000 + BandFansMax = 250000 PastConcertsToGenerate = 1 PastConcertGen = concertInCityGenerator } |> State.Concerts.addScheduledConcert @@ -258,8 +258,8 @@ let ``sold tickets decrease to 20% of the normal cap when last visit to the city let concert = State.generateOne { State.defaultOptions with - BandFansMin = 250000 - BandFansMax = 250000 + BandFansMin = 250000 + BandFansMax = 250000 PastConcertsToGenerate = 5 PastConcertGen = concertInCityGenerator } |> State.Concerts.addScheduledConcert @@ -276,8 +276,8 @@ let ``does not compute daily tickets sold as infinity when the days until the co let concert = State.generateOne { State.defaultOptions with - BandFansMin = 2500000 - BandFansMax = 2500000 } + BandFansMin = 2500000 + BandFansMax = 2500000 } |> State.Concerts.addScheduledConcert dummyBand (ScheduledConcert( @@ -295,9 +295,11 @@ let ``computes daily tickets based on headliner if participation type is opening let concert = State.generateOne { State.defaultOptions with - BandFansMin = 250 - BandFansMax = 250 } - |> State.Bands.addSimulated { dummyHeadlinerBand with Fans = 1200 } + BandFansMin = 250 + BandFansMax = 250 } + |> State.Bands.addSimulated + { dummyHeadlinerBand with + Fans = [ dummyConcert.CityId, 1200 ] |> Map.ofList } |> State.Concerts.addScheduledConcert dummyBand (ScheduledConcert( diff --git a/tests/Simulation.Tests/Concerts/Live.Finish.Tests.fs b/tests/Simulation.Tests/Concerts/Live.Finish.Tests.fs index 07c760fa..8e128cb1 100644 --- a/tests/Simulation.Tests/Concerts/Live.Finish.Tests.fs +++ b/tests/Simulation.Tests/Concerts/Live.Finish.Tests.fs @@ -16,9 +16,12 @@ open Duets.Simulation.Time let private attendance = 1000 let private calculateExpectedFanGain fans (modifier: float) = - let fanChange = float attendance * modifier |> Math.ceilToNearest + let totalFans = Queries.Bands.totalFans fans - fans + fanChange |> Math.lowerClamp 0 + let fanChange = + float attendance * modifier |> Math.ceilToNearest |> (*) 1 + + totalFans + fanChange |> Math.lowerClamp 0 let private assertFanGain modifier state band concert = let (Diff(_, updatedFans)) = @@ -29,7 +32,9 @@ let private assertFanGain modifier state band concert = | _ -> None) |> List.head - updatedFans |> should equal (calculateExpectedFanGain band.Fans modifier) + let totalFans = Queries.Bands.totalFans updatedFans + + totalFans |> should equal (calculateExpectedFanGain band.Fans modifier) let private simulateAndCheck' minConcertPoints @@ -39,8 +44,8 @@ let private simulateAndCheck' = State.generateN { State.defaultOptions with - BandFansMin = 100 - BandFansMax = 1000 } + BandFansMin = 100 + BandFansMax = 1000 } 100 |> List.iter (fun state -> let band = Queries.Bands.currentBand state diff --git a/tests/Simulation.Tests/Concerts/OpeningActOpportunities.Tests.fs b/tests/Simulation.Tests/Concerts/OpeningActOpportunities.Tests.fs index fdee1441..0e869998 100644 --- a/tests/Simulation.Tests/Concerts/OpeningActOpportunities.Tests.fs +++ b/tests/Simulation.Tests/Concerts/OpeningActOpportunities.Tests.fs @@ -20,8 +20,8 @@ let ``applyToConcertOpportunity returns NotEnoughFame if headliner's fame is mor let state = State.generateOne { State.defaultOptions with - BandFansMin = 1 - BandFansMax = 1 } + BandFansMin = 1 + BandFansMax = 1 } |> State.Bands.addCharacterBand dummyHeadlinerBand OpeningActOpportunities.applyToConcertOpportunity @@ -38,8 +38,8 @@ let ``applyToConcertOpportunity returns NotEnoughReleases if band does not have let state = State.generateOne { State.defaultOptions with - BandFansMin = 20000 - BandFansMax = 30000 } + BandFansMin = 20000 + BandFansMax = 30000 } |> State.Bands.addCharacterBand dummyHeadlinerBand OpeningActOpportunities.applyToConcertOpportunity @@ -56,8 +56,8 @@ let ``applyToConcertOpportunity returns AnotherConcertAlreadyScheduled if band a let state = State.generateOne { State.defaultOptions with - BandFansMin = 20000 - BandFansMax = 30000 } + BandFansMin = 20000 + BandFansMax = 30000 } |> State.Albums.addReleased dummyBand dummyReleasedAlbum |> State.Concerts.addScheduledConcert dummyBand @@ -80,8 +80,8 @@ let ``applyToConcertOpportunity returns ok with effects if all checks succeed`` let state = State.generateOne { State.defaultOptions with - BandFansMin = 20000 - BandFansMax = 30000 } + BandFansMin = 20000 + BandFansMax = 30000 } |> State.Albums.addReleased dummyBand dummyReleasedAlbum |> State.Bands.addCharacterBand dummyHeadlinerBand @@ -99,8 +99,8 @@ let ``applyToConcertOpportunity returns ok if band fame is higher than headliner let state = State.generateOne { State.defaultOptions with - BandFansMin = 2000000 - BandFansMax = 3000000 } + BandFansMin = 2000000 + BandFansMax = 3000000 } |> State.Albums.addReleased dummyBand dummyReleasedAlbum |> State.Bands.addCharacterBand dummyHeadlinerBand @@ -121,8 +121,8 @@ let ``generate does not create any opportunities in venues that are too big or s let initialState = State.generateOne { State.defaultOptions with - BandFansMin = 3000000 - BandFansMax = 3000000 } + BandFansMin = 3000000 + BandFansMax = 3000000 } |> State.Bands.addCharacterBand dummyHeadlinerBand let state = @@ -139,15 +139,17 @@ let ``generate does not create any opportunities in venues that are too big or s | ConcertSpace space -> space.Capacity | _ -> failwith "Concert scheduled in non-concert space" - match headliner.Fans with - | fame when fame <= 1000 -> + let totalFans = Queries.Bands.totalFans' headliner + + match totalFans with + | fans when fans <= 1000 -> venueCapacity |> should be (lessThanOrEqualTo 300) - | fame when fame <= 5000 -> + | fans when fans <= 5000 -> venueCapacity |> should be (lessThanOrEqualTo 500) - | fame when fame <= 20000 -> + | fans when fans <= 20000 -> venueCapacity |> should be (lessThanOrEqualTo 20000) venueCapacity |> should be (greaterThanOrEqualTo 500) - | fame when fame <= 100000 -> + | fans when fans <= 100000 -> venueCapacity |> should be (lessThanOrEqualTo 20000) venueCapacity |> should be (greaterThanOrEqualTo 500) | _ -> @@ -163,8 +165,8 @@ let ``generate does not create any opportunity for a band that has more than 35 let initialState = State.generateOne { State.defaultOptions with - BandFansMin = 100 - BandFansMax = 20000 } + BandFansMin = 100 + BandFansMax = 20000 } |> State.Bands.addCharacterBand dummyHeadlinerBand let state = diff --git a/tests/Simulation.Tests/Events/Band.Events.Tests.fs b/tests/Simulation.Tests/Events/Band.Events.Tests.fs index 1d29c202..0be1c5fd 100644 --- a/tests/Simulation.Tests/Events/Band.Events.Tests.fs +++ b/tests/Simulation.Tests/Events/Band.Events.Tests.fs @@ -11,7 +11,12 @@ open Duets.Entities open Duets.Simulation let bandFansChanged fansBefore fansAfter = - BandFansChanged(dummyBand, Diff(fansBefore, fansAfter)) + let createFanBase fans = + [ Prague, fans * 1 ] |> Map.ofList + + let previousFanBase = createFanBase fansBefore + let updatedFanBase = createFanBase fansAfter + BandFansChanged(dummyBand, Diff(previousFanBase, updatedFanBase)) let stateWithAlbum = State.generateOne State.defaultOptions diff --git a/tests/Simulation.Tests/Events/Character.Events.Tests.fs b/tests/Simulation.Tests/Events/Character.Events.Tests.fs index 1717a64f..10f9d85a 100644 --- a/tests/Simulation.Tests/Events/Character.Events.Tests.fs +++ b/tests/Simulation.Tests/Events/Character.Events.Tests.fs @@ -227,7 +227,11 @@ let ``tick of BandFansChanged updates the character's fame to half of the estima CharacterAttribute.Fame 8 - let effect = BandFansChanged(dummyBand, Diff(5000, 10000)) + let previousFans = [ Prague, 5000 ] |> Map.ofList + + let updatedFans = [ Prague, 10000 ] |> Map.ofList + + let effect = BandFansChanged(dummyBand, Diff(previousFans, updatedFans)) Simulation.tickOne state effect |> fst diff --git a/tests/Simulation.Tests/SocialNetworks/DailyUpdate.Tests.fs b/tests/Simulation.Tests/SocialNetworks/DailyUpdate.Tests.fs index cd7082ab..8b82465a 100644 --- a/tests/Simulation.Tests/SocialNetworks/DailyUpdate.Tests.fs +++ b/tests/Simulation.Tests/SocialNetworks/DailyUpdate.Tests.fs @@ -12,8 +12,8 @@ open Duets.Simulation.SocialNetworks.DailyUpdate let states min max = State.generateN { State.defaultOptions with - BandFansMax = max - BandFansMin = min } + BandFansMax = max * 1 + BandFansMin = min * 1 } 100 let statesWithAccounts min max characterFollowers bandFollowers = @@ -41,7 +41,7 @@ let statesWithAccounts min max characterFollowers bandFollowers = let checkDiffIsGreaterThanOrEqualTo n effect = match effect with - | SocialNetworkAccountFollowersChanged (_, _, Diff (prev, curr)) -> + | SocialNetworkAccountFollowersChanged(_, _, Diff(prev, curr)) -> curr - prev |> should be (greaterThanOrEqualTo n) | _ -> failwith "Unrecognized effect" diff --git a/tests/Test.Common/Generators/Band.Generator.fs b/tests/Test.Common/Generators/Band.Generator.fs deleted file mode 100644 index 64058349..00000000 --- a/tests/Test.Common/Generators/Band.Generator.fs +++ /dev/null @@ -1,13 +0,0 @@ -module Test.Common.Generators.Band - -open FsCheck - -open Duets.Entities - -let generator = - gen { - let! initialBand = Arb.generate - let! fans = Gen.choose (0, 1000000) - - return { initialBand with Fans = fans } - } diff --git a/tests/Test.Common/Generators/State.Generator.fs b/tests/Test.Common/Generators/State.Generator.fs index 0f6b72e0..c4b298b7 100644 --- a/tests/Test.Common/Generators/State.Generator.fs +++ b/tests/Test.Common/Generators/State.Generator.fs @@ -14,8 +14,8 @@ let dateGenerator = && date < Calendar.gameBeginning.AddYears(2)) type StateGenOptions = - { BandFansMin: int - BandFansMax: int + { BandFansMin: int + BandFansMax: int CharacterMoodMin: int CharacterMoodMax: int FutureConcertsToGenerate: int @@ -29,8 +29,8 @@ type StateGenOptions = PostGen: Gen } let defaultOptions = - { BandFansMin = 0 - BandFansMax = 25 + { BandFansMin = 0 + BandFansMax = 25 CharacterMoodMin = 100 CharacterMoodMax = 100 FutureConcertsToGenerate = 0 @@ -101,11 +101,14 @@ let generator (opts: StateGenOptions) = (cm.CharacterId, character)) - let! bandFame = Gen.choose (opts.BandFansMin, opts.BandFansMax) + let! generatedFans = + Gen.choose (opts.BandFansMin / 1, opts.BandFansMax / 1) + + let bandFans = [ Prague, generatedFans * 1 ] |> Map.ofList let band = { dummyBand with - Fans = bandFame + Fans = bandFans Members = bandMembers } return diff --git a/tests/Test.Common/Library.fs b/tests/Test.Common/Library.fs index b17a0d4d..e9ffb511 100644 --- a/tests/Test.Common/Library.fs +++ b/tests/Test.Common/Library.fs @@ -82,7 +82,7 @@ let dummyHeadlinerBand = { dummyBand with Id = Identity.create () |> BandId Name = "The Headliners" - Fans = 12000 } + Fans = [ Prague, 12000 ] |> Map.ofList } let dummySong = Song.empty diff --git a/tests/Test.Common/Test.Common.fsproj b/tests/Test.Common/Test.Common.fsproj index 11185484..3671d301 100644 --- a/tests/Test.Common/Test.Common.fsproj +++ b/tests/Test.Common/Test.Common.fsproj @@ -9,7 +9,6 @@ - From f13b9387bf1ea5bf0f2dc7cb10bad7b3f3315e7c Mon Sep 17 00:00:00 2001 From: sleepyfran Date: Mon, 21 Oct 2024 12:09:12 +0200 Subject: [PATCH 3/7] Random distribution of fans --- src/Duets.Simulation/Bands/Generation.fs | 18 ++++++++-------- src/Duets.Simulation/RandomGen.fs | 26 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/Duets.Simulation/Bands/Generation.fs b/src/Duets.Simulation/Bands/Generation.fs index d9f47ca2..26abd9ce 100644 --- a/src/Duets.Simulation/Bands/Generation.fs +++ b/src/Duets.Simulation/Bands/Generation.fs @@ -39,7 +39,7 @@ module Fans = /// - For low fame level, between 0.01 and 0.1 of the genre's market /// - For medium fame level, between 0.1 and 0.6 of the genre's market /// - For high fame level, between 0.6 and 1.0 of the genre's market - let generateForCity state fameLevel genre cityId = + let generateTotal state fameLevel genre = let marketCap = match fameLevel with | Low -> 0.0001, 0.001 @@ -53,18 +53,18 @@ module Fans = let upperBound = usefulMarket * (fst marketCap) let lowerBound = usefulMarket * (snd marketCap) - let fame = - RandomGen.choice [ upperBound; lowerBound ] - |> Math.ceilToNearest - |> (*) 1 - - cityId, fame + RandomGen.choice [ upperBound; lowerBound ] |> Math.ceilToNearest /// Generates a random number of fans given the fan level for all cities /// in the game world. let generate state fameLevel genre : FanBaseByCity = - // TODO: Distribute fans among cities. - [ generateForCity state fameLevel genre Prague ] |> Map.ofList + let totalFans = generateTotal state fameLevel genre + + let allCities = + Queries.World.allCities |> List.map (fun city -> city.Id) + + RandomGen.distribute totalFans allCities + |> Map.map (fun _ value -> value * 1) module Members = /// Generate a list of members of a band based on the usual roles that a diff --git a/src/Duets.Simulation/RandomGen.fs b/src/Duets.Simulation/RandomGen.fs index 85be9abf..ac8aeeef 100644 --- a/src/Duets.Simulation/RandomGen.fs +++ b/src/Duets.Simulation/RandomGen.fs @@ -1,5 +1,7 @@ module Duets.Simulation.RandomGen +open Duets.Common + type GenFunc = unit -> int type GenBetweenFunc = int -> int -> int @@ -7,6 +9,7 @@ type RandomGenAgentMessage = | Change of System.Random | Reset | Gen of AsyncReplyChannel + | GenDouble of AsyncReplyChannel | GenBetween of min: int * max: int * channel: AsyncReplyChannel /// Agent that encapsulates a random number generator to not have to pass a @@ -30,6 +33,9 @@ type private RandomGenAgent() = | 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 @@ -41,6 +47,8 @@ type private RandomGenAgent() = 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)) @@ -54,6 +62,8 @@ 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 = @@ -69,3 +79,19 @@ 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 From 290041fdd3f691580a5a3b220bdd5bece7a897c4 Mon Sep 17 00:00:00 2001 From: sleepyfran Date: Mon, 21 Oct 2024 15:15:14 +0200 Subject: [PATCH 4/7] Display fans on each city --- .../Phone/Apps/Statistics/BandStatistics.fs | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/Duets.Cli/Scenes/Phone/Apps/Statistics/BandStatistics.fs b/src/Duets.Cli/Scenes/Phone/Apps/Statistics/BandStatistics.fs index e834303b..695ed739 100644 --- a/src/Duets.Cli/Scenes/Phone/Apps/Statistics/BandStatistics.fs +++ b/src/Duets.Cli/Scenes/Phone/Apps/Statistics/BandStatistics.fs @@ -1,13 +1,24 @@ -module Duets.Cli.Scenes.Phone.Apps.Statistics.Band +module rec Duets.Cli.Scenes.Phone.Apps.Statistics.Band open Duets.Agents open Duets.Cli.Components open Duets.Cli.Text +open Duets.Entities open Duets.Simulation let bandStatisticsSubScene statisticsApp = let state = State.get () let band = Queries.Bands.currentBand state + let totalFans = Queries.Bands.totalFans' band + + showOverviewTable state band totalFans + + if totalFans > 0 then + showFanDetailTable band + + statisticsApp () + +let private showOverviewTable state band totalFans = let estimatedFame = Queries.Bands.estimatedFameLevel state band.Id let tableColumns = @@ -16,8 +27,6 @@ let bandStatisticsSubScene statisticsApp = Styles.header "Start Date" Styles.header "Fans around the world" ] - let totalFans = Queries.Bands.totalFans' band - let tableRows = [ Styles.title band.Name band.Genre @@ -26,4 +35,14 @@ let bandStatisticsSubScene statisticsApp = showTable tableColumns [ tableRows ] - statisticsApp () +let private showFanDetailTable band = + let tableColumns = [ Styles.header "City"; Styles.header "Fans" ] + + let tableRows = + band.Fans + |> Map.fold + (fun acc cityId fans -> + acc @ [ [ Generic.cityName cityId; fans |> Styles.number ] ]) + [] + + showTable tableColumns tableRows From 50dd3412b1275cfcb10b967a88a1f94bf5a65c2c Mon Sep 17 00:00:00 2001 From: sleepyfran Date: Mon, 21 Oct 2024 15:36:18 +0200 Subject: [PATCH 5/7] Ensure daily update always increases fans --- src/Duets.Simulation/Albums/DailyUpdate.fs | 2 +- src/Duets.Simulation/Albums/FanIncrease.fs | 12 +++- .../Albums/DailyUpdate.Tests.fs | 61 +++++++++++++------ 3 files changed, 52 insertions(+), 23 deletions(-) diff --git a/src/Duets.Simulation/Albums/DailyUpdate.fs b/src/Duets.Simulation/Albums/DailyUpdate.fs index 0b523a3e..f16ff0b0 100644 --- a/src/Duets.Simulation/Albums/DailyUpdate.fs +++ b/src/Duets.Simulation/Albums/DailyUpdate.fs @@ -27,7 +27,7 @@ let private bandDailyUpdate state bandId albumsByBand = let recalculatedHype = reduceDailyHype album let fanIncrease = calculateFanIncrease previousDayNonFanStreams - let updatedFanBase = applyFanIncrease fanIncrease band.Fans + let updatedFanBase = applyFanIncrease band fanIncrease [ yield AlbumReleasedUpdate( diff --git a/src/Duets.Simulation/Albums/FanIncrease.fs b/src/Duets.Simulation/Albums/FanIncrease.fs index d6068a66..6ae314c7 100644 --- a/src/Duets.Simulation/Albums/FanIncrease.fs +++ b/src/Duets.Simulation/Albums/FanIncrease.fs @@ -12,5 +12,13 @@ let calculateFanIncrease nonFanStreams = |> (*) 1 /// Applies the given fan increase to all cities in the fan base. -let applyFanIncrease fanIncrease (currentFans: FanBaseByCity) = - currentFans |> Map.map (fun _ fans -> fans + fanIncrease) +let applyFanIncrease band fanIncrease = + // Ensure that we always have at least one city in the fan base, otherwise + // it will be impossible to ever increase the number of fans. + let fanBase = + if band.Fans |> Map.isEmpty then + [ band.OriginCity, 0 ] |> Map.ofList + else + band.Fans + + fanBase |> Map.map (fun _ fans -> fans + fanIncrease) diff --git a/tests/Simulation.Tests/Albums/DailyUpdate.Tests.fs b/tests/Simulation.Tests/Albums/DailyUpdate.Tests.fs index 4896a3a6..9593377b 100644 --- a/tests/Simulation.Tests/Albums/DailyUpdate.Tests.fs +++ b/tests/Simulation.Tests/Albums/DailyUpdate.Tests.fs @@ -61,6 +61,27 @@ let ``dailyUpdate should return list without money transfer if quantity is 0`` let updateEffects = dailyUpdate state updateEffects |> should haveLength 1 +let private testDailyUpdate state assertFn = + let updateEffects = + state |> addReleasedAlbum state.Bands.Current album |> dailyUpdate + + let effect = + updateEffects + |> List.tryItem 2 + |> Option.orElse (List.tryItem 1 updateEffects) + |> Option.get + + let (Diff(_, updatedFanCount)) = + match effect with + | BandFansChanged(_, diff) -> diff + | _ -> failwith "That's not the type we were expecting" + + let band = Queries.Bands.currentBand state + let totalUpdatedFanCount = Queries.Bands.totalFans updatedFanCount + let previousTotalFanCount = Queries.Bands.totalFans' band + + assertFn totalUpdatedFanCount previousTotalFanCount + let private testDailyUpdateWith minFans maxFans maxFanDifference = State.generateN { State.defaultOptions with @@ -68,26 +89,9 @@ let private testDailyUpdateWith minFans maxFans maxFanDifference = BandFansMax = maxFans } 100 |> List.iter (fun state -> - let updateEffects = - state |> addReleasedAlbum state.Bands.Current album |> dailyUpdate - - let effect = - updateEffects - |> List.tryItem 2 - |> Option.orElse (List.tryItem 1 updateEffects) - |> Option.get - - let (Diff(_, updatedFanCount)) = - match effect with - | BandFansChanged(_, diff) -> diff - | _ -> failwith "That's not the type we were expecting" - - let band = Queries.Bands.currentBand state - let totalUpdatedFanCount = Queries.Bands.totalFans updatedFanCount - let previousTotalFanCount = Queries.Bands.totalFans' band - - totalUpdatedFanCount - previousTotalFanCount - |> should be (lessThanOrEqualTo maxFanDifference)) + testDailyUpdate state (fun updatedFanCount previousFanCount -> + updatedFanCount - previousFanCount + |> should be (lessThanOrEqualTo maxFanDifference))) [] let ``dailyUpdate should increase fans based on streams`` () = @@ -101,3 +105,20 @@ let ``dailyUpdate should increase fans based on streams`` () = (minFans * 1) (maxFans * 1) maxExpectedDifference) + +[] +let ``dailyUpdate increases fans based on streams even if the band has no previous fans (i.e. fan-base contains no cities)`` + () + = + State.generateN State.defaultOptions 100 + |> List.iter (fun state -> + let characterBand = Queries.Bands.currentBand state + + let stateWithEmptyFanBase = + state |> State.Bands.changeFans characterBand Map.empty + + testDailyUpdate + stateWithEmptyFanBase + (fun updatedFanCount previousFanCount -> + updatedFanCount - previousFanCount + |> should be (greaterThan 0))) From 7b9cd7c7017d045f990893049848bf8022897e2f Mon Sep 17 00:00:00 2001 From: sleepyfran Date: Tue, 22 Oct 2024 00:24:00 +0200 Subject: [PATCH 6/7] Scope concert sold tickets to region fame --- src/Duets.Simulation/Concerts/DailyUpdate.fs | 4 +-- .../Concerts/DailyUpdate.Tests.fs | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/Duets.Simulation/Concerts/DailyUpdate.fs b/src/Duets.Simulation/Concerts/DailyUpdate.fs index d1911f45..e76317d2 100644 --- a/src/Duets.Simulation/Concerts/DailyUpdate.fs +++ b/src/Duets.Simulation/Concerts/DailyUpdate.fs @@ -96,10 +96,10 @@ let private concertDailyUpdate state scheduledConcert = let lastVisitModifier = lastVisitModifier state band concert - let totalFans = Queries.Bands.totalFans' band |> float + let fansInCity = Queries.Bands.fansInCity' band concert.CityId |> float let fanAttendanceCap = - totalFans * 0.15 * ticketPriceModifier * lastVisitModifier + fansInCity * 0.15 * ticketPriceModifier * lastVisitModifier let nonFansAttendanceCap = calculateNonFansAttendanceCap diff --git a/tests/Simulation.Tests/Concerts/DailyUpdate.Tests.fs b/tests/Simulation.Tests/Concerts/DailyUpdate.Tests.fs index 4cbcae00..44dfefee 100644 --- a/tests/Simulation.Tests/Concerts/DailyUpdate.Tests.fs +++ b/tests/Simulation.Tests/Concerts/DailyUpdate.Tests.fs @@ -112,6 +112,33 @@ let ``daily sold tickets are calculated based on how many days are left until th concert.TicketsSold |> should equal 24 +let newYorkVenue = + Queries.World.placesByTypeInCity NewYork PlaceTypeIndex.ConcertSpace + |> List.head + +[] +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 + BandFansMax = 25000 } + |> State.Concerts.addScheduledConcert + dummyBand + (ScheduledConcert( + { dummyConcert 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 From 4983ee5cd332199b8819a46c67a2b1b2db916609 Mon Sep 17 00:00:00 2001 From: sleepyfran Date: Tue, 22 Oct 2024 00:35:43 +0200 Subject: [PATCH 7/7] Suitable venues scoped to region fame --- .../Scenes/Phone/Apps/ConcertAssistant/ScheduleSoloShow.fs | 2 +- src/Duets.Simulation/Concerts/OpeningActOpportunities.fs | 6 +++--- src/Duets.Simulation/Queries/Concerts.fs | 6 +++--- .../Concerts/OpeningActOpportunities.Tests.fs | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Duets.Cli/Scenes/Phone/Apps/ConcertAssistant/ScheduleSoloShow.fs b/src/Duets.Cli/Scenes/Phone/Apps/ConcertAssistant/ScheduleSoloShow.fs index bd82dab2..b1de9adf 100644 --- a/src/Duets.Cli/Scenes/Phone/Apps/ConcertAssistant/ScheduleSoloShow.fs +++ b/src/Duets.Cli/Scenes/Phone/Apps/ConcertAssistant/ScheduleSoloShow.fs @@ -75,7 +75,7 @@ and private promptForVenue app date dayMoment city = let band = Queries.Bands.currentBand (State.get ()) let _, maxCapacityRecommended = - Queries.Concerts.suitableVenueCapacity (State.get ()) band.Id + Queries.Concerts.suitableVenueCapacity (State.get ()) band.Id city.Id let venues = Queries.World.placeIdsByTypeInCity city.Id PlaceTypeIndex.ConcertSpace diff --git a/src/Duets.Simulation/Concerts/OpeningActOpportunities.fs b/src/Duets.Simulation/Concerts/OpeningActOpportunities.fs index 2d5eaff0..d0931e3f 100644 --- a/src/Duets.Simulation/Concerts/OpeningActOpportunities.fs +++ b/src/Duets.Simulation/Concerts/OpeningActOpportunities.fs @@ -46,7 +46,7 @@ let private generateOpeningActShowsOnDate state headlinerBands cityId date = let earningPercentage = calculateEarningPercentage headlinerFameLevel - let venue = findSuitableVenue state venuesInCity headliner + let venue = findSuitableVenue state cityId venuesInCity headliner let concert = Concert.create @@ -59,8 +59,8 @@ let private generateOpeningActShowsOnDate state headlinerBands cityId date = (headliner, concert)) -let private findSuitableVenue state venuesInCity band : Place = - let range = Queries.Concerts.suitableVenueCapacity state band.Id +let private findSuitableVenue state cityId venuesInCity band : Place = + let range = Queries.Concerts.suitableVenueCapacity state band.Id cityId (* We rely on the fact that there will always be a suitable venue in the city. diff --git a/src/Duets.Simulation/Queries/Concerts.fs b/src/Duets.Simulation/Queries/Concerts.fs index 289be931..28a6dbe5 100644 --- a/src/Duets.Simulation/Queries/Concerts.fs +++ b/src/Duets.Simulation/Queries/Concerts.fs @@ -137,11 +137,11 @@ let fairTicketPrice state bandId = /// Returns the range of capacity that the venue needs to have to be suitable /// for the given band. -let suitableVenueCapacity state bandId = +let suitableVenueCapacity state bandId cityId = let band = Bands.byId state bandId - let totalFans = Bands.totalFans' band + let fansInCity = Bands.fansInCity' band cityId - match totalFans with + match fansInCity with | fans when fans <= 1000 -> (0, 300) | fans when fans <= 5000 -> (0, 500) | fans when fans <= 20000 -> (500, 5000) diff --git a/tests/Simulation.Tests/Concerts/OpeningActOpportunities.Tests.fs b/tests/Simulation.Tests/Concerts/OpeningActOpportunities.Tests.fs index 0e869998..93d3207e 100644 --- a/tests/Simulation.Tests/Concerts/OpeningActOpportunities.Tests.fs +++ b/tests/Simulation.Tests/Concerts/OpeningActOpportunities.Tests.fs @@ -139,9 +139,9 @@ let ``generate does not create any opportunities in venues that are too big or s | ConcertSpace space -> space.Capacity | _ -> failwith "Concert scheduled in non-concert space" - let totalFans = Queries.Bands.totalFans' headliner + let fansInCity = Queries.Bands.fansInCity' headliner concert.CityId - match totalFans with + match fansInCity with | fans when fans <= 1000 -> venueCapacity |> should be (lessThanOrEqualTo 300) | fans when fans <= 5000 ->