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
|
namespace LivingVillage.Desktop.Tests
open System
open System.IO
open Microsoft.VisualStudio.TestTools.UnitTesting
open LivingVillage.Kernel
open LivingVillage.Kernel.Sim
open LivingVillage.Desktop
open LivingVillage.Desktop.ChineseText
/// P37 职业系统首切片验收回归(docs/design/profession-system.md §4):
/// 渔夫首项钓鱼相关、职业任务生成确定性、开局属性差异、旧档兼容、中文 UI 选择。
[<TestClass>]
type P37ProfessionTests () =
let withTempSave (action: string -> unit) =
let id = Guid.NewGuid().ToString("N")
let path = Path.Combine(Path.GetTempPath(), $"living-village-p37-{id}.save")
try
action path
finally
if File.Exists path then File.Delete path
let fisherPool : Occupation.TaskTemplateId list =
[ Occupation.NightCatch; Occupation.SellFish; Occupation.MarketInquiry ]
let sequence (seed: uint64) (kind: Occupation.Kind) (days: int64 list) : Occupation.TaskTemplateId list =
days
|> List.map (fun day ->
(Occupation.dailyTaskOf seed (Occupation.occupationSeedOf kind) day kind).Value.TemplateId)
[<TestMethod>]
member _.FisherFirstTaskIsFishingRelated () =
let state = WorldBootstrap.occupationStateFor 42UL Occupation.Fisher 0L
match state.Today with
| Some task ->
Assert.IsTrue(fisherPool |> List.contains task.TemplateId, sprintf "渔夫当日任务越池:%A" task.TemplateId)
Assert.IsTrue(
[ "夜捕"; "卖渔"; "询价" ] |> List.contains (Occupation.taskNameOf task.TemplateId),
"渔夫首项必须是钓鱼相关任务")
| None -> Assert.Fail("渔夫开局必须生成当日任务")
[<TestMethod>]
member _.SameProfessionAndSeedProduceIdenticalTaskSequences () =
let days = [ 0L .. 29L ]
for kind in Occupation.all do
let once = sequence 42UL kind days
let twice = sequence 42UL kind days
Assert.AreEqual<Occupation.TaskTemplateId list>(once, twice)
for day in days do
let task = (Occupation.dailyTaskOf 42UL (Occupation.occupationSeedOf kind) day kind).Value
Assert.IsTrue(Occupation.taskPoolOf kind |> List.contains task.TemplateId, sprintf "day %d 越池" day)
[<TestMethod>]
member _.DifferentProfessionsProduceDifferentTaskSequences () =
let days = [ 0L .. 29L ]
let fisher = sequence 42UL Occupation.Fisher days
for kind in [ Occupation.Farmer; Occupation.Peddler; Occupation.Scholar ] do
Assert.IsTrue(fisher <> sequence 42UL kind days, sprintf "渔夫与 %A 的任务序列不应相同" kind)
[<TestMethod>]
member _.OpeningMoneyAndEnergyDifferPerProfession () =
let needsOf (kind: Occupation.Kind) =
let world = WorldBootstrap.initialWorldWithOccupation false 42UL 4 (Some(Occupation.stateOf kind))
world.Avatar.Mind.Needs
Assert.AreEqual<float32>(60.0f, (needsOf Occupation.Farmer).Money)
Assert.AreEqual<float32>(45.0f, (needsOf Occupation.Fisher).Money)
Assert.AreEqual<float32>(90.0f, (needsOf Occupation.Peddler).Money)
Assert.AreEqual<float32>(70.0f, (needsOf Occupation.Scholar).Money)
Assert.AreEqual<float32>(95.0f, (needsOf Occupation.Fisher).Energy)
for kind in [ Occupation.Farmer; Occupation.Peddler; Occupation.Scholar ] do
Assert.AreEqual<float32>(100.0f, (needsOf kind).Energy)
[<TestMethod>]
member _.OpeningInventoryBundleIsDefinedPerProfession () =
Assert.AreEqual<(Sim.ItemKind * int) list>([ Sim.Food, 12 ], (Occupation.profileOf Occupation.Farmer).InitialInventory)
Assert.AreEqual<(Sim.ItemKind * int) list>(
[ Sim.Food, 8; Sim.Fish, 3 ],
(Occupation.profileOf Occupation.Fisher).InitialInventory)
Assert.AreEqual<(Sim.ItemKind * int) list>(
[ Sim.Food, 6; Sim.Spice, 4 ],
(Occupation.profileOf Occupation.Peddler).InitialInventory)
Assert.AreEqual<(Sim.ItemKind * int) list>(
[ Sim.Food, 10; Sim.Scroll, 2 ],
(Occupation.profileOf Occupation.Scholar).InitialInventory)
[<TestMethod>]
member _.LegacyAndNoProfessionSavesLoadAsNoOccupation () =
let world = Sim.initialWorldN 42UL 4
withTempSave (fun path ->
File.WriteAllText(path, WorldSave.save world)
let v2Text = File.ReadAllText path
Assert.IsTrue(v2Text.StartsWith("LV_WORLD_SAVE_V2"), "无职业存档应保持 v2")
match WorldSave.loadFromFileWith path with
| Ok (_, occupation) -> Assert.AreEqual<Occupation.State option>(None, occupation)
| Error message -> Assert.Fail(sprintf "v2 读取失败:%s" message)
let firstPipe = v2Text.IndexOf('|')
let secondPipe = v2Text.IndexOf('|', firstPipe + 1)
let thirdPipe = v2Text.IndexOf('|', secondPipe + 1)
File.WriteAllText(path, "LV_WORLD_SAVE_V1" + v2Text.Substring(thirdPipe))
match WorldSave.loadFromFileWith path with
| Ok (_, occupation) -> Assert.AreEqual<Occupation.State option>(None, occupation)
| Error message -> Assert.Fail(sprintf "v1 读取失败:%s" message))
[<TestMethod>]
member _.OccupationSelectPageNavigatesAndEmitsChosenKind () =
// 默认选中「暂不选择」(index 4);两次 Down 到「渔夫」(index 1),Enter 确认。
let page = MenuState.openOccupationSelect (MenuState.create SimulationControl.initial false)
Assert.AreEqual<MenuPage>(OccupationSelect, page.Page)
Assert.AreEqual<Occupation.Kind option>(None, MenuState.selectedOccupation page)
let fisher = MenuState.move Down (MenuState.move Down page)
Assert.AreEqual<Occupation.Kind option>(Some Occupation.Fisher, MenuState.selectedOccupation fisher)
let _, command = MenuState.update Confirm fisher
Assert.AreEqual<MenuCommand>(StartNewGameWith(Some Occupation.Fisher), command)
[<TestMethod>]
member _.OccupationUiLabelsAreAtlasBacked () =
let labels =
[ "选择职业"; "暂不选择"; Occupation.nameOf Occupation.Farmer; Occupation.nameOf Occupation.Fisher
Occupation.nameOf Occupation.Peddler; Occupation.nameOf Occupation.Scholar ]
for label in labels do
Assert.IsTrue(requiredUiLabels |> List.contains label, sprintf "缺少必备用字登记:%s" label)
Assert.IsTrue(PixelText.isRenderable label, sprintf "文案不可渲染(缺字):%s" label)
|