summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj1
-rw-r--r--src/LivingVillage.Kernel.Tests/TradeBiasTests.fs71
-rw-r--r--src/LivingVillage.Kernel/Occupation.fs21
3 files changed, 93 insertions, 0 deletions
diff --git a/src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj b/src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj
index c11e064..dea8314 100644
--- a/src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj
+++ b/src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj
@@ -13,6 +13,7 @@
<Compile Include="M5Tests.fs" />
<Compile Include="M6aTests.fs" />
<Compile Include="DailyTaskTests.fs" />
+ <Compile Include="TradeBiasTests.fs" />
<Compile Include="PerformanceTests.fs" />
</ItemGroup>
diff --git a/src/LivingVillage.Kernel.Tests/TradeBiasTests.fs b/src/LivingVillage.Kernel.Tests/TradeBiasTests.fs
new file mode 100644
index 0000000..96d3e2f
--- /dev/null
+++ b/src/LivingVillage.Kernel.Tests/TradeBiasTests.fs
@@ -0,0 +1,71 @@
+namespace LivingVillage.Kernel.Tests
+
+open Microsoft.VisualStudio.TestTools.UnitTesting
+open LivingVillage.Kernel
+open LivingVillage.Kernel.Sim
+
+module private TradeBiasHarness =
+
+ let quotedWorld () : World = Sim.initialWorldN 42UL 4
+
+ let quote (buyer: NpcId) (seller: NpcId) (item: ItemKind) (world: World) : float32 =
+ match Sim.quotePrice { Buyer = buyer; Seller = seller; Item = item; Quantity = 1 } world with
+ | Some price -> price
+ | None -> Assert.Fail("quote expected"); Unchecked.defaultof<float32>
+
+[<TestClass>]
+type TradeBiasTests () =
+
+ // ---- 默认(无职业)= 旧行为基准 ----
+
+ [<TestMethod>]
+ member _.NoOccupationKeepsTheHistoricQuoteUnchanged () =
+ Assert.AreEqual<float32>(1.0f, Occupation.quoteBiasOf None true)
+ Assert.AreEqual<float32>(1.0f, Occupation.quoteBiasOf None false)
+ Assert.AreEqual<float32 option>(Some 10.0f, Occupation.biasedQuote None true (Some 10.0f))
+ Assert.AreEqual<float32 option>(None, Occupation.biasedQuote None true None)
+
+ [<TestMethod>]
+ member _.FarmerAndFisherStayUnbiasedLikeTheDefaultPath () =
+ for kind in [ Occupation.Farmer; Occupation.Fisher ] do
+ Assert.AreEqual<float32>(1.0f, Occupation.quoteBiasOf (Some kind) true)
+ Assert.AreEqual<float32>(1.0f, Occupation.quoteBiasOf (Some kind) false)
+
+ // ---- 货郎:买卖双向有利偏移(±3.00%,4 位定点基点)----
+
+ [<TestMethod>]
+ member _.PeddlerBuysCheaperAndSellsDearer () =
+ Assert.AreEqual<float32>(0.97f, Occupation.quoteBiasOf (Some Occupation.Peddler) true)
+ Assert.AreEqual<float32>(1.03f, Occupation.quoteBiasOf (Some Occupation.Peddler) false)
+
+ // ---- 书生:解读行情的小幅加成(±1.00%)----
+
+ [<TestMethod>]
+ member _.ScholarGetsASmallerFavorableEdgeThanPeddler () =
+ let scholarBuy = Occupation.quoteBiasOf (Some Occupation.Scholar) true
+ let peddlerBuy = Occupation.quoteBiasOf (Some Occupation.Peddler) true
+ Assert.AreEqual<float32>(0.99f, scholarBuy)
+ Assert.AreEqual<float32>(1.01f, Occupation.quoteBiasOf (Some Occupation.Scholar) false)
+ Assert.IsTrue(scholarBuy > peddlerBuy, "scholar edge must stay inside the peddler edge")
+
+ // ---- 确定性与设计区间 [0.95, 1.05] ----
+
+ [<TestMethod>]
+ member _.BiasIsDeterministicAndInsideTheDesignBand () =
+ for kind in Occupation.all do
+ for playerIsBuyer in [ true; false ] do
+ let first = Occupation.quoteBiasOf (Some kind) playerIsBuyer
+ let second = Occupation.quoteBiasOf (Some kind) playerIsBuyer
+ Assert.AreEqual<float32>(first, second)
+ Assert.IsTrue(0.95f <= first && first <= 1.05f, $"{kind} bias {first} escaped the band")
+
+ // ---- 与既有 Sim.quotePrice(Food 10.0f 路径)复合 ----
+
+ [<TestMethod>]
+ member _.BiasComposesWithTheExistingQuotePath () =
+ let world = TradeBiasHarness.quotedWorld ()
+ let baseBuy = TradeBiasHarness.quote (NpcId 0) (NpcId 1) Food world
+ let baseSell = TradeBiasHarness.quote (NpcId 1) (NpcId 0) Food world
+ Assert.AreEqual<float32>(baseBuy * 0.97f, (Occupation.biasedQuote (Some Occupation.Peddler) true (Some baseBuy)).Value)
+ Assert.AreEqual<float32>(baseSell * 1.03f, (Occupation.biasedQuote (Some Occupation.Peddler) false (Some baseSell)).Value)
+ Assert.AreEqual<float32>(baseBuy, (Occupation.biasedQuote None true (Some baseBuy)).Value)
diff --git a/src/LivingVillage.Kernel/Occupation.fs b/src/LivingVillage.Kernel/Occupation.fs
index a3c37bd..5c75c81 100644
--- a/src/LivingVillage.Kernel/Occupation.fs
+++ b/src/LivingVillage.Kernel/Occupation.fs
@@ -171,3 +171,24 @@ module Occupation =
let stateOf (kind: Kind) : State =
{ Profile = profileOf kind; TaskToken = 0; Today = None; StoryStage = 0 }
+
+ // ---- 报价偏置(design §2/§3):Kernel 纯函数旁路,不改 Sim.fs 数值口径 ----
+ // 4 位定点整数基点(1/10000)表达偏置,避免浮点漂移;结果钳在 [0.95, 1.05]。
+ // 货郎:买卖双向有利(买压价/卖抬价 ±3.00%);书生:解读行情小幅加成 ±1.00%;
+ // 农夫/渔夫/无职业:恒 1.0,与旧行为逐位一致。
+
+ let private peddlerTradeBasisPoints = 300
+ let private scholarTradeBasisPoints = 100
+
+ let quoteBiasBasisPoints (kind: Kind option) (playerIsBuyer: bool) : int =
+ match kind with
+ | Some Peddler -> if playerIsBuyer then -peddlerTradeBasisPoints else peddlerTradeBasisPoints
+ | Some Scholar -> if playerIsBuyer then -scholarTradeBasisPoints else scholarTradeBasisPoints
+ | _ -> 0
+
+ let quoteBiasOf (kind: Kind option) (playerIsBuyer: bool) : float32 =
+ float32 (10000 + quoteBiasBasisPoints kind playerIsBuyer) / 10000.0f
+
+ /// Sim.quotePrice 的输出经职业偏置映射;None(无报价)原样穿透。
+ let biasedQuote (kind: Kind option) (playerIsBuyer: bool) (baseQuote: float32 option) : float32 option =
+ baseQuote |> Option.map (fun price -> price * quoteBiasOf kind playerIsBuyer)