diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-29 08:09:12 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-29 08:09:12 +0800 |
| commit | 247d0bc083dffc648f043176eabf9bd4036f19e7 (patch) | |
| tree | 78c11e2f923c7b9322b64dc81ccae86fc5bbceea /src/LivingVillage.Kernel | |
| parent | cf6672ee1d6cf7013472f3bd91e68990f23750e8 (diff) | |
| download | living-village-247d0bc083dffc648f043176eabf9bd4036f19e7.tar.gz | |
p66: 每日任务目标具体化 TargetNpc/TargetTile 定点
- Kernel Occupation:TaskTargets + targetNpcOf/targetTileOf + dailyTaskOfWith/
refreshTodayWith/refreshStateWith(splitmix 同源定点,候选排序消除顺序依赖,禁随机)
- 仅既有 satisfiesCompletion 会读目标的模板定点:帮工→TargetNpc、翻土→TargetTile;
其余 8 模板目标保持 None(不改完成语义)
- Desktop WorldBootstrap:reachableTargets(世界 NPC + 样板菜地瓦片)+
occupationStateForWorld/refreshOccupationTodayWorld;Game 开局/跨日/证据路径切换
- 历史入口 dailyTaskOf/refreshState/occupationStateFor 保留为空候选通配包装(逐字节不变)
- 红先 P66TaskTargetTests(8 例):同 seed 幂等、跨 seed 落在可达集合、候选顺序无关、
历史通配回归、帮工/翻土定点门控完成、跨日重定点、v3 round-trip + v2 基线
- scripts/p66-target-report.fsx + docs/evidence/p66-analysis.txt(PASS)
- Desktop 328/328,Kernel 119/119;digest 953775FA…7F6C ×3 PASS;Sim.fs 未动
Diffstat (limited to 'src/LivingVillage.Kernel')
| -rw-r--r-- | src/LivingVillage.Kernel/Occupation.fs | 74 |
1 files changed, 65 insertions, 9 deletions
diff --git a/src/LivingVillage.Kernel/Occupation.fs b/src/LivingVillage.Kernel/Occupation.fs index cb9e25b..58b05ec 100644 --- a/src/LivingVillage.Kernel/Occupation.fs +++ b/src/LivingVillage.Kernel/Occupation.fs @@ -169,28 +169,72 @@ module Occupation = state <- next out + // ---- P66 任务目标具体化(design §2.4-3)---- + // 候选集合由调用方注入(世界侧可达 NPC / 瓦片),Kernel 不感知地图/世界。 + // 定点走 splitmix 同源的另一段流;空候选 → None(保持通配,不臆造)。 + type TaskTargets = + { Npcs: Sim.NpcId list + Tiles: (int * int) list } + + let private targetNpcSalt = 0x9E3779B97F4A7C15UL + let private targetTileSalt = 0xD1B54A32D192ED03UL + + let private pickTargetIndex (salt: uint64) (seed: uint64) (occupationSeed: uint64) (dayIndex: int64) (count: int) : int = + int (taskHash (seed ^^^ salt) occupationSeed dayIndex % uint64 count) + + /// 按 hash 从候选 NPC 内定点(先按 id 升序消除输入顺序影响);空集 → None(通配)。 + let targetNpcOf (seed: uint64) (occupationSeed: uint64) (dayIndex: int64) (candidates: Sim.NpcId list) : Sim.NpcId option = + match candidates |> List.sortBy (fun (Sim.NpcId id) -> id) with + | [] -> None + | sorted -> Some sorted.[pickTargetIndex targetNpcSalt seed occupationSeed dayIndex sorted.Length] + + /// 按 hash 从候选瓦片内定点(先按 (x,y) 升序消除输入顺序影响);空集 → None(通配)。 + let targetTileOf (seed: uint64) (occupationSeed: uint64) (dayIndex: int64) (candidates: (int * int) list) : (int * int) option = + match candidates |> List.sort with + | [] -> None + | sorted -> Some sorted.[pickTargetIndex targetTileSalt seed occupationSeed dayIndex sorted.Length] + /// 当日任务生成:池内确定性抽 1 条;空池 → None。 - /// TargetNpc/TargetTile 首期留 None(通配),OfferedTick/DueTick 覆盖整日。 - let dailyTaskOf (seed: uint64) (occupationSeed: uint64) (dayIndex: int64) (kind: Kind) : DailyTask option = + /// 仅对既有 `satisfiesCompletion` 会读取目标的模板定点:帮工 → TargetNpc(指定村民), + /// 翻土 → TargetTile(自家地块);其余模板目标保持 None(不新增完成语义)。 + /// 候选集合为空时目标为 None,落到既有通配行为。 + let dailyTaskOfWith (targets: TaskTargets) (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 template = pool.[index] + let targetNpc = + match template with + | HelpWork -> targetNpcOf seed occupationSeed dayIndex targets.Npcs + | _ -> None + let targetTile = + match template with + | TillSoil -> targetTileOf seed occupationSeed dayIndex targets.Tiles + | _ -> None let offered = dayIndex * Sim.ticksPerDay Some - { TemplateId = pool.[index] - TargetNpc = None - TargetTile = None + { TemplateId = template + TargetNpc = targetNpc + TargetTile = targetTile OfferedTick = offered DueTick = offered + Sim.ticksPerDay - 1L State = Offered } + /// 历史口径:无候选集合 → 目标恒 None(通配)。既有调用点/测试逐字节不变。 + let dailyTaskOf (seed: uint64) (occupationSeed: uint64) (dayIndex: int64) (kind: Kind) : DailyTask option = + dailyTaskOfWith { Npcs = []; Tiles = [] } seed occupationSeed dayIndex kind + /// 当日任务刷新:已持任务的 OfferedTick 仍落在 nowTick 所属日时原样保留(含 /// Done/Failed 终态,保证当日面板与存档稳定);跨日才按当日重新生成 Offered。 - let refreshToday (seed: uint64) (occupationSeed: uint64) (kind: Kind) (nowTick: int64) (today: DailyTask option) : DailyTask option = + let refreshTodayWith (targets: TaskTargets) (seed: uint64) (occupationSeed: uint64) (kind: Kind) (nowTick: int64) (today: DailyTask option) : DailyTask option = match today with | Some task when dayIndexOf task.OfferedTick = dayIndexOf nowTick -> Some task - | _ -> dailyTaskOf seed occupationSeed (dayIndexOf nowTick) kind + | _ -> dailyTaskOfWith targets seed occupationSeed (dayIndexOf nowTick) kind + + /// 历史口径:无候选集合刷新(目标恒 None)。 + let refreshToday (seed: uint64) (occupationSeed: uint64) (kind: Kind) (nowTick: int64) (today: DailyTask option) : DailyTask option = + refreshTodayWith { Npcs = []; Tiles = [] } seed occupationSeed kind nowTick today /// 挂在玩家侧的职业状态,随存档 v3 保存(design §2)。 /// Today = 当日任务(§3);TaskToken 保留为 v3 前缀占位扩展位(恒 0,暂未启用)。 @@ -295,8 +339,16 @@ module Occupation = /// 跨日刷新职业状态(P55):重算当日任务,并按「上一日是否已完成」保留/清零连续天数。 /// 同日刷新原样保留 Streak(含 Done 终态),避免每帧刷新把 streak 冲掉。 - let refreshState (seed: uint64) (occupationSeed: uint64) (kind: Kind) (nowTick: int64) (state: State) : State = - let today = refreshToday seed occupationSeed kind nowTick state.Today + /// P66:`targets` 提供候选集合时,跨日重算的当日任务按候选定点;空候选 → 通配(历史口径)。 + let refreshStateWith + (targets: TaskTargets) + (seed: uint64) + (occupationSeed: uint64) + (kind: Kind) + (nowTick: int64) + (state: State) + : State = + let today = refreshTodayWith targets seed occupationSeed kind nowTick state.Today let sameDay = match state.Today with | Some task -> dayIndexOf task.OfferedTick = dayIndexOf nowTick @@ -310,6 +362,10 @@ module Occupation = | None -> false { state with Today = today; Streak = (if continuing then state.Streak else 0) } + /// 历史口径:无候选集合刷新(目标恒 None,逐字节不变)。 + let refreshState (seed: uint64) (occupationSeed: uint64) (kind: Kind) (nowTick: int64) (state: State) : State = + refreshStateWith { Npcs = []; Tiles = [] } seed occupationSeed kind nowTick state + // ---- 报价偏置(design §2/§3):Kernel 纯函数旁路,不改 Sim.fs 数值口径 ---- // 4 位定点整数基点(1/10000)表达偏置,避免浮点漂移;结果钳在 [0.95, 1.05]。 // 货郎:买卖双向有利(买压价/卖抬价 ±3.00%);书生:解读行情小幅加成 ±1.00%; |
