diff options
Diffstat (limited to 'src/LivingVillage.Kernel.Tests/DailyTaskTests.fs')
| -rw-r--r-- | src/LivingVillage.Kernel.Tests/DailyTaskTests.fs | 191 |
1 files changed, 191 insertions, 0 deletions
diff --git a/src/LivingVillage.Kernel.Tests/DailyTaskTests.fs b/src/LivingVillage.Kernel.Tests/DailyTaskTests.fs new file mode 100644 index 0000000..12bdf68 --- /dev/null +++ b/src/LivingVillage.Kernel.Tests/DailyTaskTests.fs @@ -0,0 +1,191 @@ +namespace LivingVillage.Kernel.Tests + +open System +open System.IO +open Microsoft.VisualStudio.TestTools.UnitTesting +open LivingVillage.Kernel +open LivingVillage.Kernel.Sim + +module private DailyTaskHarness = + + let taskOf (template: Occupation.TaskTemplateId) (state: Occupation.TaskState) : Occupation.DailyTask = + { TemplateId = template + TargetNpc = None + TargetTile = None + OfferedTick = 0L + DueTick = Sim.ticksPerDay - 1L + State = state } + + let accepted (template: Occupation.TaskTemplateId) : Occupation.DailyTask = + Occupation.acceptTask (taskOf template Occupation.Offered) + +[<TestClass>] +type DailyTaskTests () = + + // ---- §3 生成:hash(seed ^ occupationSeed, dayIndex) mod |pool|(splitmix 同源)---- + + [<TestMethod>] + member _.TaskHashFollowsSplitmixOverSeedXorOccupationSeed () = + Assert.AreEqual<uint64>(0xF7E9F3F88CC04AD6UL, Occupation.taskHash 42UL 7UL 0L) + Assert.AreEqual<uint64>(0xFB624BD98F2BE6C4UL, Occupation.taskHash 42UL 7UL 1L) + Assert.AreEqual<uint64>(0x87FC3F1DAC740225UL, Occupation.taskHash 42UL 7UL 2L) + Assert.AreEqual<uint64>(0xDA258F1151868B40UL, Occupation.taskHash 42UL 7UL 3L) + Assert.AreEqual<uint64>(0x02D114B7ABDD7A1FUL, Occupation.taskHash 4242UL 99UL 0L) + + [<TestMethod>] + member _.FarmerAndFisherPoolsAreThreeTemplatesEach () = + Assert.AreEqual( + [ Occupation.DeliverGrain; Occupation.HelpWork; Occupation.TillSoil ], + Occupation.taskPoolOf Occupation.Farmer) + Assert.AreEqual( + [ Occupation.NightCatch; Occupation.SellFish; Occupation.MarketInquiry ], + Occupation.taskPoolOf Occupation.Fisher) + Assert.AreEqual<int>(0, Occupation.taskPoolOf Occupation.Peddler |> List.length) + Assert.AreEqual<int>(0, Occupation.taskPoolOf Occupation.Scholar |> List.length) + + [<TestMethod>] + member _.GeneratedTasksStayInPoolAndRepeatDeterministically () = + for seed, occ in [ (42UL, 7UL); (7UL, 42UL); (123456789UL, 555UL) ] do + for kind in [ Occupation.Farmer; Occupation.Fisher ] do + let pool = Occupation.taskPoolOf kind + for day in 0L .. 299L do + let task = (Occupation.dailyTaskOf seed occ day kind).Value + Assert.IsTrue(pool |> List.contains task.TemplateId, $"day {day} escaped the pool") + let again = (Occupation.dailyTaskOf seed occ day kind).Value + Assert.AreEqual<Occupation.DailyTask>(task, again) + let distinct = + [ 0L .. 299L ] + |> List.map (fun day -> (Occupation.dailyTaskOf seed occ day kind).Value.TemplateId) + |> Set.ofList + Assert.IsTrue(distinct.Count >= 2, "300 days must not pin a single template") + + [<TestMethod>] + member _.PeddlerAndScholarHaveNoDailyTaskYet () = + Assert.AreEqual<Occupation.DailyTask option>(None, Occupation.dailyTaskOf 42UL 7UL 0L Occupation.Peddler) + Assert.AreEqual<Occupation.DailyTask option>(None, Occupation.dailyTaskOf 42UL 7UL 0L Occupation.Scholar) + + [<TestMethod>] + member _.TaskWindowCoversExactlyOneDayStartingOffered () = + let task = (Occupation.dailyTaskOf 42UL 7UL 3L Occupation.Farmer).Value + Assert.AreEqual<int64>(3L * Sim.ticksPerDay, task.OfferedTick) + Assert.AreEqual<int64>(4L * Sim.ticksPerDay - 1L, task.DueTick) + Assert.AreEqual<Occupation.TaskState>(Occupation.Offered, task.State) + Assert.AreEqual<int64>(3L, Occupation.dayIndexOf (3L * Sim.ticksPerDay)) + Assert.AreEqual<int64>(3L, Occupation.dayIndexOf (4L * Sim.ticksPerDay - 1L)) + + [<TestMethod>] + member _.TaskTemplateChineseNamesMatchDesignTable () = + Assert.AreEqual<string>("送粮", Occupation.taskNameOf Occupation.DeliverGrain) + Assert.AreEqual<string>("帮工", Occupation.taskNameOf Occupation.HelpWork) + Assert.AreEqual<string>("翻土", Occupation.taskNameOf Occupation.TillSoil) + Assert.AreEqual<string>("夜捕", Occupation.taskNameOf Occupation.NightCatch) + Assert.AreEqual<string>("卖渔", Occupation.taskNameOf Occupation.SellFish) + Assert.AreEqual<string>("询价", Occupation.taskNameOf Occupation.MarketInquiry) + + // ---- §3 状态机 Offered|Active|Done|Failed ---- + + [<TestMethod>] + member _.AcceptMovesOfferedToActiveOnly () = + let offered = DailyTaskHarness.taskOf Occupation.HelpWork Occupation.Offered + let active = Occupation.acceptTask offered + Assert.AreEqual<Occupation.TaskState>(Occupation.Active, active.State) + Assert.AreEqual<Occupation.TaskState>(Occupation.Active, (Occupation.acceptTask active).State) + let finished = { active with State = Occupation.Done } + Assert.AreEqual<Occupation.TaskState>(Occupation.Done, (Occupation.acceptTask finished).State) + let failed = { active with State = Occupation.Failed } + Assert.AreEqual<Occupation.TaskState>(Occupation.Failed, (Occupation.acceptTask failed).State) + + [<TestMethod>] + member _.SellFishCompletesOnlyOnPlayerFishSales () = + let active = DailyTaskHarness.accepted Occupation.SellFish + Assert.AreEqual<Occupation.TaskState>( + Occupation.Done, + (Occupation.applySignal (Occupation.Traded(playerId, Fish, 1)) active).State) + Assert.AreEqual<Occupation.TaskState>( + Occupation.Active, + (Occupation.applySignal (Occupation.Traded(playerId, Spice, 1)) active).State) + Assert.AreEqual<Occupation.TaskState>( + Occupation.Active, + (Occupation.applySignal (Occupation.Traded(NpcId 2, Fish, 4)) active).State) + let offered = DailyTaskHarness.taskOf Occupation.SellFish Occupation.Offered + Assert.AreEqual<Occupation.TaskState>( + Occupation.Offered, + (Occupation.applySignal (Occupation.Traded(playerId, Fish, 1)) offered).State) + + [<TestMethod>] + member _.DeliverGrainCompletesOnlyOnPlayerFoodGiveaways () = + let active = DailyTaskHarness.accepted Occupation.DeliverGrain + Assert.AreEqual<Occupation.TaskState>( + Occupation.Done, + (Occupation.applySignal (Occupation.Traded(playerId, Food, 3)) active).State) + Assert.AreEqual<Occupation.TaskState>( + Occupation.Active, + (Occupation.applySignal (Occupation.Traded(playerId, Fish, 1)) active).State) + Assert.AreEqual<Occupation.TaskState>( + Occupation.Active, + (Occupation.applySignal (Occupation.Traded(NpcId 5, Food, 1)) active).State) + + [<TestMethod>] + member _.HelpWorkAndTillSoilRespectOptionalTargets () = + let wildcard = DailyTaskHarness.accepted Occupation.HelpWork + Assert.AreEqual<Occupation.TaskState>( + Occupation.Done, + (Occupation.applySignal (Occupation.Dialogued(NpcId 4)) wildcard).State) + let targeted = + { DailyTaskHarness.accepted Occupation.HelpWork with TargetNpc = Some(NpcId 2) } + Assert.AreEqual<Occupation.TaskState>( + Occupation.Done, + (Occupation.applySignal (Occupation.Dialogued(NpcId 2)) targeted).State) + Assert.AreEqual<Occupation.TaskState>( + Occupation.Active, + (Occupation.applySignal (Occupation.Dialogued(NpcId 4)) targeted).State) + let untilled = DailyTaskHarness.accepted Occupation.TillSoil + Assert.AreEqual<Occupation.TaskState>( + Occupation.Done, + (Occupation.applySignal (Occupation.ArrivedAt(1, 1)) untilled).State) + let plotTilling = + { DailyTaskHarness.accepted Occupation.TillSoil with TargetTile = Some(52, 38) } + Assert.AreEqual<Occupation.TaskState>( + Occupation.Done, + (Occupation.applySignal (Occupation.ArrivedAt(52, 38)) plotTilling).State) + Assert.AreEqual<Occupation.TaskState>( + Occupation.Active, + (Occupation.applySignal (Occupation.ArrivedAt(10, 10)) plotTilling).State) + + [<TestMethod>] + member _.NightCatchAndMarketInquiryCompleteOnTheirSignals () = + let active = DailyTaskHarness.accepted Occupation.NightCatch + Assert.AreEqual<Occupation.TaskState>( + Occupation.Done, + (Occupation.applySignal Occupation.NightAtWater active).State) + Assert.AreEqual<Occupation.TaskState>( + Occupation.Active, + (Occupation.applySignal (Occupation.Dialogued(NpcId 0)) active).State) + let inquiring = DailyTaskHarness.accepted Occupation.MarketInquiry + Assert.AreEqual<Occupation.TaskState>( + Occupation.Done, + (Occupation.applySignal (Occupation.Dialogued(NpcId 3)) inquiring).State) + Assert.AreEqual<Occupation.TaskState>( + Occupation.Active, + (Occupation.applySignal Occupation.NightAtWater inquiring).State) + + [<TestMethod>] + member _.DoneAndFailedAreTerminalStates () = + let finished = { DailyTaskHarness.accepted Occupation.SellFish with State = Occupation.Done } + Assert.AreEqual<Occupation.TaskState>( + Occupation.Done, + (Occupation.applySignal (Occupation.Traded(playerId, Fish, 1)) finished).State) + let failed = { DailyTaskHarness.accepted Occupation.SellFish with State = Occupation.Failed } + Assert.AreEqual<Occupation.TaskState>( + Occupation.Failed, + (Occupation.applySignal (Occupation.Traded(playerId, Fish, 1)) failed).State) + + [<TestMethod>] + member _.OverdueTasksFailAtDayBoundaryButNotBefore () = + let active = DailyTaskHarness.accepted Occupation.SellFish + Assert.AreEqual<Occupation.TaskState>(Occupation.Active, (Occupation.expireAt active.DueTick active).State) + Assert.AreEqual<Occupation.TaskState>(Occupation.Failed, (Occupation.expireAt (active.DueTick + 1L) active).State) + let offered = DailyTaskHarness.taskOf Occupation.SellFish Occupation.Offered + Assert.AreEqual<Occupation.TaskState>(Occupation.Failed, (Occupation.expireAt (offered.DueTick + 1L) offered).State) + let finished = { active with State = Occupation.Done } + Assert.AreEqual<Occupation.TaskState>(Occupation.Done, (Occupation.expireAt (finished.DueTick + 1L) finished).State) |
