summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop.Tests
diff options
context:
space:
mode:
Diffstat (limited to 'src/LivingVillage.Desktop.Tests')
-rw-r--r--src/LivingVillage.Desktop.Tests/DesktopTests.fs119
-rw-r--r--src/LivingVillage.Desktop.Tests/M6bTests.fs30
-rw-r--r--src/LivingVillage.Desktop.Tests/PrototypeTests.fs6
3 files changed, 152 insertions, 3 deletions
diff --git a/src/LivingVillage.Desktop.Tests/DesktopTests.fs b/src/LivingVillage.Desktop.Tests/DesktopTests.fs
index 52443e9..00e445b 100644
--- a/src/LivingVillage.Desktop.Tests/DesktopTests.fs
+++ b/src/LivingVillage.Desktop.Tests/DesktopTests.fs
@@ -208,3 +208,122 @@ type TaskPanelTests () =
Assert.AreEqual<M5View>(dialogueView, afterTask)
let _, closed = M5Interaction.apply ClosePanel world { dialogueView with Panel = TaskPanel }
Assert.AreEqual<M5Panel>(WorldPanel, closed.Panel)
+
+// ---- P15 开局职业选择接入:bootstrap 职业状态 / 存档 round-trip / 面板联调 ----
+
+module private OccupationFlowHarness =
+
+ let withTempSave (action: string -> unit) =
+ let id = System.Guid.NewGuid().ToString("N")
+ let path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"living-village-p15-{id}.save")
+ try
+ action path
+ finally
+ if System.IO.File.Exists path then System.IO.File.Delete path
+
+[<TestClass>]
+type OccupationFlowTests () =
+
+ [<TestMethod>]
+ member _.BootstrapStampsOccupationMoneyEnergyAndTodaysOfferedTask () =
+ for kind in Occupation.all do
+ let state = WorldBootstrap.occupationStateFor 42UL kind 0L
+ let task = state.Today.Value
+ Assert.AreEqual<Occupation.TaskState>(Occupation.Offered, task.State)
+ Assert.IsTrue(Occupation.taskPoolOf kind |> List.contains task.TemplateId)
+ Assert.AreEqual<Occupation.DailyTask>(
+ (Occupation.dailyTaskOf 42UL (Occupation.occupationSeedOf kind) 0L kind).Value,
+ task)
+ let world = WorldBootstrap.initialWorldWithOccupation true 42UL 4 (Some state)
+ Assert.AreEqual<float32>(float32 state.Profile.InitialMoney, world.Avatar.Mind.Needs.Money)
+ Assert.AreEqual<float32>(state.Profile.InitialEnergy, world.Avatar.Mind.Needs.Energy)
+
+ [<TestMethod>]
+ member _.ChosenOccupationAndTodaysTaskRoundTripThroughV3Save () =
+ for kind in Occupation.all do
+ let state = WorldBootstrap.occupationStateFor 42UL kind (3L * Sim.ticksPerDay + 120L)
+ let world = WorldBootstrap.initialWorldWithOccupation false 42UL 4 (Some state)
+ OccupationFlowHarness.withTempSave (fun path ->
+ WorldSave.saveToFileWith (Some state) path world
+ let text = System.IO.File.ReadAllText path
+ Assert.IsTrue(text.StartsWith("LV_WORLD_SAVE_V3"), "chosen occupation must save as v3")
+ match WorldSave.loadFromFileWith path with
+ | Ok (restoredWorld, Some restoredState) ->
+ Assert.AreEqual<Occupation.Kind>(kind, restoredState.Profile.Kind)
+ Assert.AreEqual<Occupation.State>(state, restoredState)
+ Assert.AreEqual<string>(text, WorldSave.saveWith (Some restoredState) restoredWorld)
+ | Ok (_, None) -> Assert.Fail($"v3 occupation lost for {kind}")
+ | Error message -> Assert.Fail($"v3 round trip failed for {kind}: {message}"))
+
+ [<TestMethod>]
+ member _.TaskPanelShowsTheChosenOccupationsOfferedTask () =
+ let world = Sim.initialWorldN 42UL 4
+ for kind in Occupation.all do
+ let state = WorldBootstrap.occupationStateFor 42UL kind 0L
+ let view = { M5Interaction.initial with Task = Some state }
+ let _, taskView = M5Interaction.apply ShowTaskPanel world view
+ let lines = M5Interaction.panelLines world taskView
+ Assert.IsTrue(lines |> List.contains (Occupation.nameOf kind), $"occupation name missing for {kind}")
+ Assert.IsTrue(
+ lines |> List.exists (fun line -> line.Contains(Occupation.taskNameOf state.Today.Value.TemplateId)),
+ $"task name missing for {kind}")
+ Assert.IsTrue(lines |> List.exists (fun line -> line.Contains("待接受")), $"offered state missing for {kind}")
+
+ [<TestMethod>]
+ member _.RefreshOccupationTodayKeepsSameDayAndRegeneratesForTheNewDay () =
+ let state = WorldBootstrap.occupationStateFor 42UL Occupation.Farmer 0L
+ Assert.AreEqual<Occupation.State>(state, WorldBootstrap.refreshOccupationToday 42UL state (Sim.ticksPerDay - 1L))
+ let day1 = WorldBootstrap.refreshOccupationToday 42UL state Sim.ticksPerDay
+ Assert.AreEqual<Occupation.DailyTask option>(
+ Some (Occupation.dailyTaskOf 42UL (Occupation.occupationSeedOf Occupation.Farmer) 1L Occupation.Farmer).Value,
+ day1.Today)
+
+ [<TestMethod>]
+ member _.ChosenOccupationFirstDayTaskReachesDoneThroughItsSignal () =
+ // 既有事件通道:交易/对话模板由 InteractionEvent 经 signalOfInteraction 推进;
+ // 到达瓦片/夜水边/观察由调用方折叠为信号(Sim.step 不感知)。
+ let eventOf (template: Occupation.TaskTemplateId) : Sim.InteractionEvent option =
+ match template with
+ | Occupation.DeliverGrain ->
+ Some { Tick = 0L; Kind = Sim.TradeEvent(NpcId 1, Sim.playerId, Food, 1, 1.0f) }
+ | Occupation.SellFish ->
+ Some { Tick = 0L; Kind = Sim.TradeEvent(NpcId 1, Sim.playerId, Fish, 1, 1.0f) }
+ | Occupation.Resell ->
+ Some { Tick = 0L; Kind = Sim.TradeEvent(NpcId 1, Sim.playerId, Spice, 1, 1.0f) }
+ | Occupation.BuyGoods ->
+ Some { Tick = 0L; Kind = Sim.TradeEvent(Sim.playerId, NpcId 1, Spice, 1, 1.0f) }
+ | Occupation.HelpWork
+ | Occupation.MarketInquiry
+ | Occupation.ReasonDebate ->
+ Some
+ { Tick = 0L
+ Kind =
+ Sim.DialogueEvent
+ { Tick = 0L
+ Actor = Sim.playerId
+ Target = NpcId 0
+ Intent = SmallTalk
+ Response = Friendly
+ Valence = 0.3f } }
+ | Occupation.TillSoil
+ | Occupation.NightCatch
+ | Occupation.ObserveNotes -> None
+ let foldedSignal (template: Occupation.TaskTemplateId) : Occupation.TaskSignal =
+ match template with
+ | Occupation.TillSoil -> Occupation.ArrivedAt(52, 38)
+ | Occupation.NightCatch -> Occupation.NightAtWater
+ | Occupation.ObserveNotes -> Occupation.Observed
+ | _ -> failwith "event-backed template"
+ for kind in Occupation.all do
+ let state = WorldBootstrap.occupationStateFor 42UL kind 0L
+ let offered = state.Today.Value
+ Assert.AreEqual<Occupation.TaskState>(Occupation.Offered, offered.State)
+ Assert.IsTrue(Occupation.taskPoolOf kind |> List.contains offered.TemplateId)
+ let active = { state with Today = Some(Occupation.acceptTask offered) }
+ let completed =
+ match eventOf offered.TemplateId with
+ | Some event -> Occupation.advanceTask event active
+ | None ->
+ { active with
+ Today = active.Today |> Option.map (Occupation.applySignal (foldedSignal offered.TemplateId)) }
+ Assert.AreEqual<Occupation.TaskState>(Occupation.Done, completed.Today.Value.State)
diff --git a/src/LivingVillage.Desktop.Tests/M6bTests.fs b/src/LivingVillage.Desktop.Tests/M6bTests.fs
index 6ae0634..5aea9bd 100644
--- a/src/LivingVillage.Desktop.Tests/M6bTests.fs
+++ b/src/LivingVillage.Desktop.Tests/M6bTests.fs
@@ -80,3 +80,33 @@ type M6bMenuTests () =
Assert.AreEqual<string option>(Some "missing save", failed.Error)
Assert.AreEqual<MenuPage>(MainMenu, returned.Page)
Assert.AreEqual<MenuCommand>(NoCommand, command)
+
+ // ---- P15 开局职业选择页 ----
+
+ [<TestMethod>]
+ member _.OccupationSelectOffersFourKindsThenNoneAndDefaultsToNone () =
+ let page = MenuState.openOccupationSelect (MenuState.create SimulationControl.initial false)
+
+ Assert.AreEqual<MenuPage>(OccupationSelect, page.Page)
+ Assert.AreEqual<Occupation.Kind option list>(
+ [ Some Occupation.Farmer; Some Occupation.Fisher; Some Occupation.Peddler; Some Occupation.Scholar; None ],
+ MenuState.occupationOptions)
+ Assert.AreEqual<Occupation.Kind option>(None, MenuState.selectedOccupation page)
+ Assert.AreEqual<string>("暂不选择", MenuState.occupationLabel None)
+ Assert.AreEqual<string>("农夫", MenuState.occupationLabel (Some Occupation.Farmer))
+ Assert.AreEqual<string>("书生", MenuState.occupationLabel (Some Occupation.Scholar))
+
+ [<TestMethod>]
+ member _.OccupationSelectConfirmEmitsChosenKindAndBackReturnsToMenu () =
+ let page = MenuState.openOccupationSelect (MenuState.create SimulationControl.initial false)
+ let _, defaultCommand = MenuState.update Confirm page
+ Assert.AreEqual<MenuCommand>(StartNewGameWith None, defaultCommand)
+
+ let picked = MenuState.move Up (MenuState.move Up page)
+ Assert.AreEqual<Occupation.Kind option>(Some Occupation.Peddler, MenuState.selectedOccupation picked)
+ let _, command = MenuState.update Confirm picked
+ Assert.AreEqual<MenuCommand>(StartNewGameWith (Some Occupation.Peddler), command)
+
+ let back, backCommand = MenuState.update Back page
+ Assert.AreEqual<MenuPage>(MainMenu, back.Page)
+ Assert.AreEqual<MenuCommand>(NoCommand, backCommand)
diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
index eeafb56..658e2e1 100644
--- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
+++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
@@ -945,14 +945,14 @@ type PrototypeTests () =
[<TestMethod>]
member _.CjkGlyphAtlasHasAStableCharacterTableAndGeometry () =
- Assert.AreEqual<int>(197, characters.Length)
+ Assert.AreEqual<int>(228, characters.Length)
CollectionAssert.AreEqual(characters, characters |> Array.sort)
Assert.AreEqual<int>(characters.Length, characters |> Array.distinct |> Array.length)
Assert.AreEqual<int>(16, columns)
Assert.AreEqual<int>(24, cellPixels)
- Assert.AreEqual<int>(13, rowCount)
+ Assert.AreEqual<int>(15, rowCount)
Assert.AreEqual<int>(384, pixelWidth)
- Assert.AreEqual<int>(312, pixelHeight)
+ Assert.AreEqual<int>(360, pixelHeight)
Assert.AreEqual<string>("Assets/cjk-glyph-atlas.png", assetRelativePath)
Assert.AreEqual<string>("Assets/cjk-glyphs.txt", manifestRelativePath)