diff options
Diffstat (limited to 'src/LivingVillage.Kernel/Occupation.fs')
| -rw-r--r-- | src/LivingVillage.Kernel/Occupation.fs | 118 |
1 files changed, 118 insertions, 0 deletions
diff --git a/src/LivingVillage.Kernel/Occupation.fs b/src/LivingVillage.Kernel/Occupation.fs index 28008f5..e0507ae 100644 --- a/src/LivingVillage.Kernel/Occupation.fs +++ b/src/LivingVillage.Kernel/Occupation.fs @@ -51,3 +51,121 @@ module Occupation = let stateOf (kind: Kind) : State = { Profile = profileOf kind; TaskToken = 0; StoryStage = 0 } + + // ---- 每日任务(design §3):Kernel 侧纯函数片,不进 Sim.step 数值路径 ---- + + type TaskState = + | Offered + | Active + | Done + | Failed + + /// 首期任务池 = 农夫3(送粮/帮工/翻土)+ 渔夫3(夜捕/卖渔/询价),design §3 表。 + type TaskTemplateId = + | DeliverGrain + | HelpWork + | TillSoil + | NightCatch + | SellFish + | MarketInquiry + + let taskNameOf (template: TaskTemplateId) : string = + match template with + | DeliverGrain -> "送粮" + | HelpWork -> "帮工" + | TillSoil -> "翻土" + | NightCatch -> "夜捕" + | SellFish -> "卖渔" + | MarketInquiry -> "询价" + + /// 货郎/书生模板留后续单,当前为空池 → 不生成每日任务。 + let taskPoolOf (kind: Kind) : TaskTemplateId list = + match kind with + | Farmer -> [ DeliverGrain; HelpWork; TillSoil ] + | Fisher -> [ NightCatch; SellFish; MarketInquiry ] + | Peddler + | Scholar -> [] + + type DailyTask = + { TemplateId: TaskTemplateId + TargetNpc: Sim.NpcId option + TargetTile: (int * int) option + OfferedTick: int64 + DueTick: int64 + State: TaskState } + + /// 完成判定信号(design §3:走既有事件通道)。 + /// Dialogued 来自 DialogueEvent(chooseDialogue);Traded 来自 TradeEvent, + /// 字段序 = 卖方/物品/数量(玩家递出即 seller = playerId);ArrivedAt 为到达瓦片; + /// NightAtWater 由调用方把 isNight + 水边判定折叠后喂入。Sim.step 零感知。 + type TaskSignal = + | Dialogued of Sim.NpcId + | Traded of Sim.NpcId * Sim.ItemKind * int + | ArrivedAt of int * int + | NightAtWater + + /// TargetNpc/TargetTile 为 None 时视为通配(指定目标由后续 Desktop 接线单落地)。 + let satisfiesCompletion (task: DailyTask) (signal: TaskSignal) : bool = + match task.TemplateId, signal with + | DeliverGrain, Traded (seller, item, quantity) -> seller = Sim.playerId && item = Sim.Food && quantity >= 1 + | HelpWork, Dialogued npc -> + match task.TargetNpc with + | Some target -> target = npc + | None -> true + | TillSoil, ArrivedAt (tileX, tileY) -> + match task.TargetTile with + | Some target -> target = (tileX, tileY) + | None -> true + | NightCatch, NightAtWater -> true + | SellFish, Traded (seller, item, quantity) -> seller = Sim.playerId && item = Sim.Fish && quantity >= 1 + | MarketInquiry, Dialogued _ -> true + | _ -> false + + /// Offered → Active;其余状态原样返回(终态不可复活)。 + let acceptTask (task: DailyTask) : DailyTask = + if task.State = Offered then { task with State = Active } else task + + /// 仅 Active 下命中完成信号才置 Done;Offered 须先 accept,Done/Failed 终态原样返回。 + let applySignal (signal: TaskSignal) (task: DailyTask) : DailyTask = + if task.State = Active && satisfiesCompletion task signal then + { task with State = Done } + else + task + + /// 逾期判负:DueTick 之后仍未完成 → Failed;Done/Failed 原样返回。 + let expireAt (nowTick: int64) (task: DailyTask) : DailyTask = + if nowTick > task.DueTick && task.State <> Done && task.State <> Failed then + { task with State = Failed } + else + task + + let dayIndexOf (tick: int64) : int64 = tick / Sim.ticksPerDay + + /// hash(seed ^ occupationSeed, dayIndex) —— 与 Rng.fs splitmix 同源(禁 System.Random)。 + /// 输出只依赖三元组,与调用顺序无关:seed 异或 occupationSeed 后连续推进 + /// dayIndex+1 次 splitmix,末次输出即当日哈希。 + let taskHash (seed: uint64) (occupationSeed: uint64) (dayIndex: int64) : uint64 = + let mutable state = Rng.ofSeed (seed ^^^ occupationSeed) + let mutable out = 0UL + if dayIndex >= 0L then + for _ in 1L .. dayIndex + 1L do + let value, next = Rng.nextUInt64 state + out <- value + state <- next + out + + /// 当日任务生成:池内确定性抽 1 条;空池(货郎/书生首期)→ None。 + /// TargetNpc/TargetTile 首期留 None(通配),OfferedTick/DueTick 覆盖整日。 + let dailyTaskOf (seed: uint64) (occupationSeed: uint64) (dayIndex: int64) (kind: Kind) : DailyTask option = + match taskPoolOf kind with + | [] -> None + | pool -> + let index = int (taskHash seed occupationSeed dayIndex % uint64 pool.Length) + let offered = dayIndex * Sim.ticksPerDay + Some + { TemplateId = pool.[index] + TargetNpc = None + TargetTile = None + OfferedTick = offered + DueTick = offered + Sim.ticksPerDay - 1L + State = Offered } |
