diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj | 1 | ||||
| -rw-r--r-- | src/LivingVillage.Kernel.Tests/TradeBiasTests.fs | 71 | ||||
| -rw-r--r-- | src/LivingVillage.Kernel/Occupation.fs | 21 |
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) |
