summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-29 08:09:12 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-29 08:09:12 +0800
commit247d0bc083dffc648f043176eabf9bd4036f19e7 (patch)
tree78c11e2f923c7b9322b64dc81ccae86fc5bbceea /src/LivingVillage.Desktop
parentcf6672ee1d6cf7013472f3bd91e68990f23750e8 (diff)
downloadliving-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.Desktop')
-rw-r--r--src/LivingVillage.Desktop/Game.fs17
-rw-r--r--src/LivingVillage.Desktop/WorldBootstrap.fs39
2 files changed, 51 insertions, 5 deletions
diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs
index 862e1de..a0e6fc0 100644
--- a/src/LivingVillage.Desktop/Game.fs
+++ b/src/LivingVillage.Desktop/Game.fs
@@ -551,12 +551,17 @@ type LivingVillageGame() as this =
member private this.StartNewGame (occupation: Occupation.Kind option) =
let startTick = evidenceStartTick ()
- let state =
+ let baseState =
occupation
|> Option.map (fun kind -> WorldBootstrap.occupationStateFor 42UL kind startTick)
world <-
- initialWorldForMode state
+ initialWorldForMode baseState
|> withStartTick
+ // P66:用真实世界的可达候选集合给当日任务定点(世界只依赖 profile,故只需建一次)。
+ let state =
+ match occupation with
+ | Some kind -> Some(WorldBootstrap.occupationStateForWorld 42UL kind startTick world)
+ | None -> None
// §3 身份条目:带职业开局即写入年鉴,形成个人传记线
match state with
| Some occupationState ->
@@ -1665,7 +1670,7 @@ type LivingVillageGame() as this =
setDay3Noon ()
// 用真实跨日刷新把当日任务推进到第 3 日(seed 42/农夫 = 送粮)。
match m5View.Task with
- | Some state -> m5View <- { m5View with Task = Some(WorldBootstrap.refreshOccupationToday 42UL state world.Tick) }
+ | Some state -> m5View <- { m5View with Task = Some(WorldBootstrap.refreshOccupationTodayWorld 42UL state world.Tick world) }
| None -> ()
let npc = world.Npcs.[0]
world <- { world with Avatar = { world.Avatar with Pos = npc.Pos } }
@@ -2013,7 +2018,7 @@ type LivingVillageGame() as this =
this.CenterCamera()
match m5View.Task with
| Some state ->
- let refreshed = WorldBootstrap.refreshOccupationToday 42UL state world.Tick
+ let refreshed = WorldBootstrap.refreshOccupationTodayWorld 42UL state world.Tick world
if refreshed <> state then
m5View <- { m5View with Task = Some refreshed }
| None -> ()
@@ -2120,13 +2125,15 @@ type LivingVillageGame() as this =
let startTick = lineStartDay * Sim.ticksPerDay + 12L * Sim.ticksPerDay / 24L
let state = WorldBootstrap.occupationStateFor seed kind startTick
let world0 = WorldBootstrap.initialWorldWithOccupation false seed Sim.npcCount (Some state)
+ // P66:证据路径同样按真实世界候选定点当日任务(world0 只依赖 profile)。
+ let task0 = WorldBootstrap.occupationStateForWorld seed kind startTick world0
let world1 = { world0 with Tick = startTick; Time = float startTick * Sim.dtSeconds }
let world2 =
Sim.appendAnnal
{ Tick = startTick; Kind = StoryAnnal; Summary = Occupation.Story.identitySummaryOf kind }
world1
let mutable w = world2
- let mutable task = state
+ let mutable task = task0
for i in 0 .. stage - 1 do
let npc = w.Npcs.[i % w.Npcs.Length]
let wNear = { w with Avatar = { w.Avatar with Pos = npc.Pos } }
diff --git a/src/LivingVillage.Desktop/WorldBootstrap.fs b/src/LivingVillage.Desktop/WorldBootstrap.fs
index e75855f..e0ac037 100644
--- a/src/LivingVillage.Desktop/WorldBootstrap.fs
+++ b/src/LivingVillage.Desktop/WorldBootstrap.fs
@@ -29,6 +29,45 @@ module WorldBootstrap =
let refreshOccupationToday (seed: uint64) (state: Occupation.State) (nowTick: int64) : Occupation.State =
Occupation.refreshState seed (Occupation.occupationSeedOf state.Profile.Kind) state.Profile.Kind nowTick state
+ // ---- P66 任务目标具体化(design §2.4-3)----
+ // 可达候选集合完全由世界/样板场景派生;Kernel 只做纯 hash 定点。
+ /// 可达 NPC:世界中实际存在的村民 id(顺序稳定)。
+ /// 可达瓦片:样板场景的菜地(VegetableGarden)世界瓦片 —— 设计口径「自家地块」,
+ /// 跨 legacy/riverscape 一致且必在陆地上。
+ let reachableTargets (world: World) : Occupation.TaskTargets =
+ let npcs = world.Npcs |> Array.map (fun npc -> npc.Id) |> Array.toList
+ let tiles =
+ VillagePresentation.scene.Elements
+ |> List.choose (fun element ->
+ match element.Prop with
+ | VillagePresentation.VegetableGarden ->
+ let tile = VillagePresentation.worldTileForSceneTile element.Position
+ Some(tile.X, tile.Y)
+ | _ -> None)
+ { Npcs = npcs; Tiles = tiles }
+
+ /// 新档职业状态(P66):按真实世界的可达候选集合定点当日任务。
+ let occupationStateForWorld (seed: uint64) (kind: Occupation.Kind) (nowTick: int64) (world: World) : Occupation.State =
+ let baseState = Occupation.stateOf kind
+ { baseState with
+ Today =
+ Occupation.dailyTaskOfWith
+ (reachableTargets world)
+ seed
+ (Occupation.occupationSeedOf kind)
+ (Occupation.dayIndexOf nowTick)
+ kind }
+
+ /// 跨日刷新(P66):按真实世界的可达候选集合重算跨日当天任务;同日原样保留。
+ let refreshOccupationTodayWorld (seed: uint64) (state: Occupation.State) (nowTick: int64) (world: World) : Occupation.State =
+ Occupation.refreshStateWith
+ (reachableTargets world)
+ seed
+ (Occupation.occupationSeedOf state.Profile.Kind)
+ state.Profile.Kind
+ nowTick
+ state
+
/// 开局身份年鉴(design §3):带职业建世界后写入一条 `StoryAnnal`「以…为生」,
/// 形成个人传记线。Game 与测试共用同一口径。
let appendIdentityAnnal (state: Occupation.State) (world: World) : World =