diff --git a/src/Duets.Cli/Components/Commands/Cheats/Cheats.Commands.fs b/src/Duets.Cli/Components/Commands/Cheats/Cheats.Commands.fs new file mode 100644 index 00000000..cc7e9272 --- /dev/null +++ b/src/Duets.Cli/Components/Commands/Cheats/Cheats.Commands.fs @@ -0,0 +1,8 @@ +namespace Duets.Cli.Components.Commands.Cheats + +module Index = + let all = + [ LifeCommands.happy + MoneyCommands.motherlode + MoneyCommands.rosebud + TriggerEffectCommands.triggerEffect ] diff --git a/src/Duets.Cli/Components/Commands/Cheats/Index.fs b/src/Duets.Cli/Components/Commands/Cheats/Index.fs deleted file mode 100644 index ab3b40dd..00000000 --- a/src/Duets.Cli/Components/Commands/Cheats/Index.fs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Duets.Cli.Components.Commands.Cheats - -module Index = - let all = - [ LifeCommands.happy; MoneyCommands.motherlode; MoneyCommands.rosebud ] diff --git a/src/Duets.Cli/Components/Commands/Cheats/TriggerEffect.Cheats.Commands.fs b/src/Duets.Cli/Components/Commands/Cheats/TriggerEffect.Cheats.Commands.fs new file mode 100644 index 00000000..06df3eab --- /dev/null +++ b/src/Duets.Cli/Components/Commands/Cheats/TriggerEffect.Cheats.Commands.fs @@ -0,0 +1,31 @@ +namespace Duets.Cli.Components.Commands.Cheats + +open Duets.Agents +open Duets.Cli +open Duets.Cli.Components.Commands +open Duets.Cli.SceneIndex +open Duets.Entities +open Duets.Simulation + +[] +module TriggerEffectCommands = + /// Command which triggers the specified effect. Since this requires manually + /// pattern matching against all possible effects, it does not support + /// every effect. + let triggerEffect = + { Name = "trigger_effect" + Description = "" + Handler = + (fun args -> + match args with + | [ "band_fans"; oldFans; newFans ] -> + let currentBand = Queries.Bands.currentBand (State.get ()) + + BandFansChanged( + currentBand, + Diff(int oldFans, int newFans) + ) + |> Effect.apply + | _ -> () + + Scene.World) } diff --git a/src/Duets.Cli/Duets.Cli.fsproj b/src/Duets.Cli/Duets.Cli.fsproj index cc8e6ac9..ac7ac477 100644 --- a/src/Duets.Cli/Duets.Cli.fsproj +++ b/src/Duets.Cli/Duets.Cli.fsproj @@ -7,7 +7,7 @@ Cli - + @@ -59,27 +59,27 @@ - - + + - - - - - - - + + + + + + + - - - - - - - - - - + + + + + + + + + + @@ -104,7 +104,8 @@ - + + diff --git a/src/Duets.Simulation/Albums/ReviewGeneration.fs b/src/Duets.Simulation/Albums/ReviewGeneration.fs index 07f905c6..f0d29594 100644 --- a/src/Duets.Simulation/Albums/ReviewGeneration.fs +++ b/src/Duets.Simulation/Albums/ReviewGeneration.fs @@ -58,7 +58,7 @@ let private generateReviewsForAlbum (band: Band) releasedAlbum = |> Tuple.two band |> AlbumReviewsReceived -let private generateReviewsForBand state bandId albums = +let private generateReviewsForBandAlbums state bandId albums = let band = Queries.Bands.byId state bandId let fanBase = band.Fans @@ -78,9 +78,15 @@ let private generateReviewsForBand state bandId albums = /// yet and generates them based on the score of the album and the source of the /// review, only if the band has the minimum amount of fame required to have /// reviews generated for them. -let generateReviews (state: State) : Effect list = +let generateReviewsForLatestAlbums (state: State) = Queries.Albums.releaseInLast state 3 |> Map.fold (fun acc bandId albums -> - acc @ generateReviewsForBand state bandId albums) + acc @ generateReviewsForBandAlbums state bandId albums) [] + +/// Retrieves all albums released by bands that have the minimum amount of fame +/// and generates the reviews for all their albums. +let generateReviewsForBand state bandId = + Queries.Albums.releasedByBand state bandId + |> generateReviewsForBandAlbums state bandId diff --git a/src/Duets.Simulation/Duets.Simulation.fsproj b/src/Duets.Simulation/Duets.Simulation.fsproj index be74dc5d..6a5008a0 100644 --- a/src/Duets.Simulation/Duets.Simulation.fsproj +++ b/src/Duets.Simulation/Duets.Simulation.fsproj @@ -100,23 +100,25 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/src/Duets.Simulation/Events/Band/Band.Events.fs b/src/Duets.Simulation/Events/Band/Band.Events.fs new file mode 100644 index 00000000..1a100bf3 --- /dev/null +++ b/src/Duets.Simulation/Events/Band/Band.Events.fs @@ -0,0 +1,18 @@ +module Duets.Simulation.Events.Band.Band + +open Duets.Entities +open Duets.Simulation +open Duets.Simulation.Events + +/// Runs all the events associated with bands. For example, when the fan base +/// 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 + | _ -> None diff --git a/src/Duets.Simulation/Events/Band/Reviews.fs b/src/Duets.Simulation/Events/Band/Reviews.fs new file mode 100644 index 00000000..02ca4c12 --- /dev/null +++ b/src/Duets.Simulation/Events/Band/Reviews.fs @@ -0,0 +1,6 @@ +module Duets.Simulation.Events.Band.Reviews + +open Duets.Simulation.Albums + +let generateReviewsAfterFanIncrease bandId state = + ReviewGeneration.generateReviewsForBand state bandId diff --git a/src/Duets.Simulation/Events/Events.fs b/src/Duets.Simulation/Events/Events.fs index a235d1e1..59336738 100644 --- a/src/Duets.Simulation/Events/Events.fs +++ b/src/Duets.Simulation/Events/Events.fs @@ -1,11 +1,12 @@ module Duets.Simulation.Events.Events open Duets.Entities +open Duets.Simulation.Events.Band open Duets.Simulation.Events.Character /// Retrieves all associated effects with the given one. let associatedEffects effect = - [ Time.run effect; Skill.run effect; Character.run effect ] + [ Time.run effect; Skill.run effect; Character.run effect; Band.run effect ] |> List.choose id /// Retrieves all the effects that have to happen at the end of an effect chain. diff --git a/src/Duets.Simulation/Events/Time.Events.fs b/src/Duets.Simulation/Events/Time.Events.fs index 8424dc5a..2a054c91 100644 --- a/src/Duets.Simulation/Events/Time.Events.fs +++ b/src/Duets.Simulation/Events/Time.Events.fs @@ -17,7 +17,7 @@ let private runDailyEffects time state = match Calendar.Query.dayMomentOf time with | Morning -> Albums.DailyUpdate.dailyUpdate state - @ Albums.ReviewGeneration.generateReviews state + @ Albums.ReviewGeneration.generateReviewsForLatestAlbums state @ Concerts.DailyUpdate.dailyUpdate state | Midday -> SocialNetworks.DailyUpdate.dailyUpdate state | _ -> [] diff --git a/tests/Simulation.Tests/Albums/ReviewGeneration.Tests.fs b/tests/Simulation.Tests/Albums/ReviewGeneration.Tests.fs index 238be9a8..dd6af14c 100644 --- a/tests/Simulation.Tests/Albums/ReviewGeneration.Tests.fs +++ b/tests/Simulation.Tests/Albums/ReviewGeneration.Tests.fs @@ -53,7 +53,7 @@ let ``generateReviews should return empty if band has not released any albums`` { State.defaultOptions with BandFansMin = Config.MusicSimulation.minimumFanBaseForReviews BandFansMax = 10000 } - |> generateReviews + |> generateReviewsForLatestAlbums |> should haveLength 0 [] @@ -68,7 +68,7 @@ let ``generateReviews should return empty if band does not have the minimum requ |> List.iter (fun state -> state |> addReleasedAlbum dummyBand.Id album - |> generateReviews + |> generateReviewsForLatestAlbums |> should haveLength 0) [] @@ -82,7 +82,7 @@ let ``generateReviews should return empty if band does not have any albums relea BandFansMin = Config.MusicSimulation.minimumFanBaseForReviews BandFansMax = 10000 } |> addAlbumReleasedDaysAgo days - |> generateReviews + |> generateReviewsForLatestAlbums |> should haveLength 0) [] @@ -95,7 +95,7 @@ let ``generateReviews should return empty if the band's albums already have revi BandFansMax = 10000 } 50 |> List.iter (fun state -> - state |> addAlbumWithReviews |> generateReviews |> should haveLength 0) + state |> addAlbumWithReviews |> generateReviewsForLatestAlbums |> should haveLength 0) [] let ``generateReviews should return effects if the day was three days ago regardless of the time`` @@ -115,7 +115,7 @@ let ``generateReviews should return effects if the day was three days ago regard |> addReleasedAlbum dummyBand.Id { album with ReleaseDate = releaseDate } - |> generateReviews + |> generateReviewsForLatestAlbums |> should haveLength 1) [] @@ -127,7 +127,7 @@ let ``generateReviews should return effects for each album released three days a BandFansMin = Config.MusicSimulation.minimumFanBaseForReviews BandFansMax = 10000 } |> addAlbumWithNoReviews - |> generateReviews + |> generateReviewsForLatestAlbums |> should haveLength 1 let private testReviewScore reviewerId assertFn quality = @@ -137,7 +137,7 @@ let private testReviewScore reviewerId assertFn quality = BandFansMin = Config.MusicSimulation.minimumFanBaseForReviews BandFansMax = 10000 } |> addAlbumWithQuality quality - |> generateReviews + |> generateReviewsForLatestAlbums let review = match List.head effects with diff --git a/tests/Simulation.Tests/Events/Band.Events.Tests.fs b/tests/Simulation.Tests/Events/Band.Events.Tests.fs new file mode 100644 index 00000000..96b3cc21 --- /dev/null +++ b/tests/Simulation.Tests/Events/Band.Events.Tests.fs @@ -0,0 +1,102 @@ +module Duets.Simulation.Tests.Events.Band + +open FsCheck +open FsUnit +open NUnit.Framework +open Test.Common +open Test.Common.Generators + +open Duets.Entities +open Duets.Simulation + +let bandFansChanged fansBefore fansAfter = + BandFansChanged(dummyBand, Diff(fansBefore, fansAfter)) + +let stateWithAlbum = + State.generateOne State.defaultOptions + |> State.Albums.addReleased dummyBand dummyReleasedAlbum + +let filterReviewsReceived = + List.filter (function + | AlbumReviewsReceived _ -> true + | _ -> false) + +[] +let ``tick of band fans changed should not generate reviews if count hasn't gotten past the minimum`` + () + = + Gen.choose (0, Config.MusicSimulation.minimumFanBaseForReviews - 2) + |> Gen.sample 0 100 + |> List.iter (fun previousFans -> + Simulation.tickOne + stateWithAlbum + (bandFansChanged previousFans (previousFans + 1)) + |> fst + |> filterReviewsReceived + |> should haveLength 0) + +[] +let ``tick of band fans changed should not generate reviews if count has already gotten past the minimum before`` + () + = + Gen.choose ( + Config.MusicSimulation.minimumFanBaseForReviews + 1, + Config.MusicSimulation.minimumFanBaseForReviews + 10000 + ) + |> Gen.sample 0 100 + |> List.iter (fun previousFans -> + Simulation.tickOne + stateWithAlbum + (bandFansChanged previousFans (previousFans + 1)) + |> fst + |> filterReviewsReceived + |> should haveLength 0) + +[] +let ``tick of band fans changed should generate reviews if count has just gotten past the minimum`` + () + = + Gen.choose ( + Config.MusicSimulation.minimumFanBaseForReviews + 1, + Config.MusicSimulation.minimumFanBaseForReviews + 10000 + ) + |> Gen.sample 0 100 + |> List.iter (fun updatedFans -> + Simulation.tickOne + stateWithAlbum + (bandFansChanged + (Config.MusicSimulation.minimumFanBaseForReviews - 1) + updatedFans) + |> fst + |> filterReviewsReceived + |> should haveLength 1) + +[] +let ``tick of band fans changed should generate reviews for all previously released albums`` + () + = + let secondUnreleasedAlbum = + Album.Unreleased.from dummyBand "Test Album 2" dummyRecordedSongRef + + let secondAlbum = + Album.Released.fromUnreleased secondUnreleasedAlbum dummyToday 1.0 + + let thirdUnreleasedAlbum = + Album.Unreleased.from dummyBand "Test Album 3" dummyRecordedSongRef + + let thirdAlbum = + Album.Released.fromUnreleased thirdUnreleasedAlbum dummyToday 1.0 + + let state = + stateWithAlbum + |> State.Albums.addReleased dummyBand secondAlbum + |> State.Albums.addReleased dummyBand thirdAlbum + + Simulation.tickOne + state + (bandFansChanged + (Config.MusicSimulation.minimumFanBaseForReviews - 1) + (Config.MusicSimulation.minimumFanBaseForReviews + 1)) + |> fst + |> filterReviewsReceived + |> should haveLength 3 diff --git a/tests/Simulation.Tests/Simulation.Tests.fsproj b/tests/Simulation.Tests/Simulation.Tests.fsproj index b0e28f28..e6488b1d 100644 --- a/tests/Simulation.Tests/Simulation.Tests.fsproj +++ b/tests/Simulation.Tests/Simulation.Tests.fsproj @@ -55,6 +55,7 @@ +