summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-29 06:51:21 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-29 06:51:21 +0800
commit2da1cbaf253245c437800e80b9aed9e8e47306b5 (patch)
tree29f1c383f0870db743221e08fde2d158f82a40b6 /src/LivingVillage.Desktop
parenta2103b24ae6f78da6c8906cbc49599850dcc49c3 (diff)
downloadliving-village-2da1cbaf253245c437800e80b9aed9e8e47306b5.tar.gz
p65: 职业切片五 货郎打听行情读 World.Rumors(中文行情短答 + 稿证据)
- Kernel Occupation 新增 freshReliableRumors/inquiryRumor/marketInquiryText(纯判定+文案, 对齐 Sim.latestRumorFor 新鲜度/可信度口径,全序命中规则,禁随机;不改 Sim.fs) - Desktop chooseIntent:货郎 OfferTrade 读对话前谣言池,Status=inquiry|<短答>;statusLabel 透出 - 真字补 17 字(东头西北巷口盐巴米布茶柴涨两文落些)并重出 atlas(340 字/384x528) - 红先 P65RumorInquiryTests(6 例):命中/幂等/陈旧·微弱·空池/仅货郎/非货郎回归逐字节/ save v2 头不变+v3 round-trip/穷举组合无缺字 - LV_P65_SHOT 钩子 + scripts/analyze-p65.py + docs/evidence/p65-*(PASS) - Desktop 320/320,Kernel 119/119;digest 953775FA…7F6C ×3 PASS
Diffstat (limited to 'src/LivingVillage.Desktop')
-rw-r--r--src/LivingVillage.Desktop/Assets/cjk-glyph-atlas.pngbin85104 -> 89701 bytes
-rw-r--r--src/LivingVillage.Desktop/Assets/cjk-glyphs.txt17
-rw-r--r--src/LivingVillage.Desktop/CjkGlyphAtlas.fs2
-rw-r--r--src/LivingVillage.Desktop/Game.fs87
-rw-r--r--src/LivingVillage.Desktop/Interaction.fs16
5 files changed, 118 insertions, 4 deletions
diff --git a/src/LivingVillage.Desktop/Assets/cjk-glyph-atlas.png b/src/LivingVillage.Desktop/Assets/cjk-glyph-atlas.png
index ed1bf37..69662c0 100644
--- a/src/LivingVillage.Desktop/Assets/cjk-glyph-atlas.png
+++ b/src/LivingVillage.Desktop/Assets/cjk-glyph-atlas.png
Binary files differ
diff --git a/src/LivingVillage.Desktop/Assets/cjk-glyphs.txt b/src/LivingVillage.Desktop/Assets/cjk-glyphs.txt
index 0191fc0..5c37717 100644
--- a/src/LivingVillage.Desktop/Assets/cjk-glyphs.txt
+++ b/src/LivingVillage.Desktop/Assets/cjk-glyphs.txt
@@ -4,6 +4,8 @@
不
与
业
+东
+两
中
为
主
@@ -16,6 +18,7 @@
事
于
互
+些
交
人
今
@@ -58,6 +61,7 @@
助
匀
包
+北
单
卖
南
@@ -67,6 +71,7 @@
友
取
受
+口
只
可
各
@@ -88,6 +93,7 @@
天
夫
失
+头
好
始
子
@@ -108,7 +114,10 @@
差
己
已
+巴
+巷
币
+布
帮
常
平
@@ -155,6 +164,7 @@
效
敌
数
+文
料
新
方
@@ -181,6 +191,7 @@
板
林
果
+柴
标
格
档
@@ -193,6 +204,7 @@
江
没
法
+涨
淡
清
渔
@@ -210,6 +222,7 @@
留
白
的
+盐
目
看
真
@@ -224,6 +237,7 @@
竹
笑
第
+米
粮
精
系
@@ -246,10 +260,13 @@
能
自
范
+茶
获
菜
+落
衅
行
+西
观
认
讨
diff --git a/src/LivingVillage.Desktop/CjkGlyphAtlas.fs b/src/LivingVillage.Desktop/CjkGlyphAtlas.fs
index 8b42596..ab03039 100644
--- a/src/LivingVillage.Desktop/CjkGlyphAtlas.fs
+++ b/src/LivingVillage.Desktop/CjkGlyphAtlas.fs
@@ -11,7 +11,7 @@ module CjkGlyphAtlas =
// Keep this table sorted and stable so the generated PNG has a reproducible layout.
let private characterTable =
- "×一上不与业中为主乐乡书买了争事于互交人今从以件价任众会传位体作使保信倍停先入关内册再农冬准几出分切到前剧力功务动助匀包单卖南卷原去友取受只可各同和品回围圃土在坏墙备夏多夜大天夫失好始子存完定家察对将小尚就层居屋工差己已币帮常平年庄库应庙度开式当录待得心必态怒总恼情愁意戏成或户打找报拒择指挑捕据接提播操收改效敌数料新方无日旧时明昏易春是晓晚晨景暂更最有未村束板林果标格档桥歉段民水求江没法淡清渔游热照物状玩理瓦生用田留白的目看真石确社离秋移稳空竹笑第粮精系紧纭纷线结给绝继绪续置翻耕聊职背能自范获菜衅行观认讨议记讲论设访试话询说请读谅谈败货贩资走足路跳车转轴过近返还进迫退送选速道邻郎都里量金鉴钱门闭问闯闲间闻附雨零需面页项须食饥饿香鱼黄黛,:"
+ "×一上不与业东两中为主乐乡书买了争事于互些交人今从以件价任众会传位体作使保信倍停先入关内册再农冬准几出分切到前剧力功务动助匀包北单卖南卷原去友取受口只可各同和品回围圃土在坏墙备夏多夜大天夫失头好始子存完定家察对将小尚就层居屋工差己已巴巷币布帮常平年庄库应庙度开式当录待得心必态怒总恼情愁意戏成或户打找报拒择指挑捕据接提播操收改效敌数文料新方无日旧时明昏易春是晓晚晨景暂更最有未村束板林果柴标格档桥歉段民水求江没法涨淡清渔游热照物状玩理瓦生用田留白的盐目看真石确社离秋移稳空竹笑第米粮精系紧纭纷线结给绝继绪续置翻耕聊职背能自范茶获菜落衅行西观认讨议记讲论设访试话询说请读谅谈败货贩资走足路跳车转轴过近返还进迫退送选速道邻郎都里量金鉴钱门闭问闯闲间闻附雨零需面页项须食饥饿香鱼黄黛,:"
let characters = characterTable.ToCharArray()
let rowCount = (characters.Length + columns - 1) / columns
diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs
index 6b52225..862e1de 100644
--- a/src/LivingVillage.Desktop/Game.fs
+++ b/src/LivingVillage.Desktop/Game.fs
@@ -337,6 +337,12 @@ type LivingVillageGame() as this =
let mutable p64PendingName = ""
let mutable p64Pending = false
let mutable p64MoveFrames = 0
+ // P65 证据钩子(职业切片五):货郎真实开局 → 与村民真实闲聊写入既有 World.Rumors →
+ // 真实 Interact 打开对话菜单(拍菜单帧)→ 选「提议交易」询价,拍带中文行情短答的帧。
+ let p65ShotMode = Environment.GetEnvironmentVariable("LV_P65_SHOT") = "1"
+ let mutable p65Step = 0
+ let mutable p65PendingName = ""
+ let mutable p65Pending = false
// P45 证据钩子:LV_P45_SHOT=1 时拍角色 idle / 行走朝下 / 行走朝右 三帧(真实游戏截图)。
let p45ShotMode = Environment.GetEnvironmentVariable("LV_P45_SHOT") = "1"
let mutable p45Step = 0
@@ -1158,6 +1164,75 @@ type LivingVillageGame() as this =
p64PendingName world.Tick world.Avatar.Pos.X world.Avatar.Pos.Y camera.X camera.Y m5View.HomeMode
p64Step <- p64Step + 1
+ /// P65 证据:货郎真实开局 → 闲聊写入既有谣言池 → 真实 Interact 开菜单 → 提议交易询价。
+ member private this.PrepareP65Shot () =
+ let setHour (hour: float) =
+ let tick = M6Presentation.resolveStartTick (Some hour) false
+ world <- { world with Tick = tick; Time = float tick * Sim.dtSeconds }
+ let ask (command: M5Command) =
+ let nextWorld, nextView = M5Interaction.apply command world m5View
+ world <- nextWorld
+ m5View <- nextView
+ let placeAtNpc () =
+ let npc = world.Npcs.[0]
+ world <- { world with Avatar = { world.Avatar with Pos = npc.Pos } }
+ if not p65Pending then
+ match p65Step with
+ | 0 ->
+ setHour 12.0
+ placeAtNpc ()
+ ask M5Command.Interact
+ this.CenterCamera()
+ p65PendingName <- "p65-menu"
+ p65Pending <- true
+ | 1 ->
+ // 真实闲聊一次:把玩家对话写入既有 World.Rumors(最新在前),供询价读取。
+ ask M5Command.Intent1
+ placeAtNpc ()
+ ask M5Command.Interact
+ // 非货郎回归:同世界跑农夫 Interact + Intent3,应仍得旧 dialogue 文案(无 inquiry|)。
+ let farmerView =
+ { M5Interaction.initial with
+ Task = Some(Occupation.stateOf Occupation.Farmer)
+ Seed = m5View.Seed }
+ let farmerWorld, farmerOpen = M5Interaction.apply M5Command.Interact world farmerView
+ let _, farmerAnswered = M5Interaction.apply M5Command.Intent3 farmerWorld farmerOpen
+ printfn "p65-regression status=%s" farmerAnswered.Status
+ // 同一世界两次查询:证明同输入逐字符串一致(禁随机)。
+ let answer1 = Occupation.marketInquiryText (Some Occupation.Peddler) Sim.OfferTrade world
+ let answer2 = Occupation.marketInquiryText (Some Occupation.Peddler) Sim.OfferTrade world
+ printfn
+ "p65-inquiry rumors=%d fresh=%d hit=%b text1=%s text2=%s equal=%b"
+ world.Rumors.Length
+ (Occupation.freshReliableRumors world |> List.length)
+ (Occupation.inquiryRumor world |> Option.isSome)
+ (answer1 |> Option.defaultValue "<none>")
+ (answer2 |> Option.defaultValue "<none>")
+ (answer1 = answer2)
+ p65Step <- p65Step + 1
+ | 2 ->
+ ask M5Command.Intent3
+ p65PendingName <- "p65-answer"
+ p65Pending <- true
+ | _ ->
+ printfn "p65-shot=done"
+ this.Exit()
+
+ /// P65 证据:落盘并打印帧名/面板/状态,供 analyze 核验「货郎 status=inquiry|...」。
+ member private this.CaptureP65Shot () =
+ if p65Pending then
+ p65Pending <- false
+ System.IO.Directory.CreateDirectory recordDirectory |> ignore
+ this.SaveBackBuffer(sprintf "%s/%s.png" recordDirectory p65PendingName)
+ printfn
+ "p65-shot=%s panel=%s avatar=(%.0f,%.0f) status=%s"
+ p65PendingName
+ (M5Interaction.panelName m5View)
+ world.Avatar.Pos.X
+ world.Avatar.Pos.Y
+ m5View.Status
+ p65Step <- p65Step + 1
+
member private this.PrepareP45Shot () =
let baseTick = M6Presentation.resolveStartTick (Some 12.0) false
let setTick (tick: int64) =
@@ -2163,6 +2238,16 @@ type LivingVillageGame() as this =
else this.DispatchMenuInput Confirm
elif menu.Page = Playing then
this.PrepareP64Shot()
+ elif p65ShotMode then
+ if menu.Page = MainMenu then
+ this.DispatchMenuInput Confirm
+ elif menu.Page = OccupationSelect then
+ let targetIndex =
+ MenuState.occupationOptions |> List.findIndex (fun option -> option = Some Occupation.Peddler)
+ if menu.Selected <> targetIndex then this.DispatchMenuInput Down
+ else this.DispatchMenuInput Confirm
+ elif menu.Page = Playing then
+ this.PrepareP65Shot()
elif p45ShotMode then
if menu.Page = MainMenu then
this.DispatchMenuInput Confirm
@@ -2639,6 +2724,8 @@ type LivingVillageGame() as this =
this.CaptureP62Shot()
if p64ShotMode && menu.Page = Playing then
this.CaptureP64Shot()
+ if p65ShotMode && menu.Page = Playing then
+ this.CaptureP65Shot()
if p45ShotMode && menu.Page = Playing then
this.CaptureP45Shot()
if p46ShotMode then
diff --git a/src/LivingVillage.Desktop/Interaction.fs b/src/LivingVillage.Desktop/Interaction.fs
index 75575dd..ba792d1 100644
--- a/src/LivingVillage.Desktop/Interaction.fs
+++ b/src/LivingVillage.Desktop/Interaction.fs
@@ -318,6 +318,12 @@ module M5Interaction =
match view.Task with
| Some task -> Occupation.biasResponse (Some task.Profile.Kind)
| None -> Sim.responseFor
+ // P65:货郎打听行情读**对话前**既有谣言池,产出中文行情短答。
+ // 非货郎 / 无职业 / 无命中谣言 → None,落回既有 dialogue 文案(旧行为逐字节不变)。
+ let inquiry =
+ match view.Task with
+ | Some task -> Occupation.marketInquiryText (Some task.Profile.Kind) intent world
+ | None -> None
match Sim.chooseDialogueWith respond menu.Target intent world with
| DialogueSucceeded(outcome, next) ->
// P-d 剧情线:每次玩家对话至多推进一段,摘要写年鉴(StoryAnnal)。
@@ -352,9 +358,12 @@ module M5Interaction =
PromptTargetPos = None
Task = advanced
Status =
- match taskMessage with
- | Some message -> "task|" + message
- | None -> sprintf "dialogue intent=%A response=%A" outcome.Intent outcome.Response
+ match inquiry with
+ | Some text -> "inquiry|" + text
+ | None ->
+ match taskMessage with
+ | Some message -> "task|" + message
+ | None -> sprintf "dialogue intent=%A response=%A" outcome.Intent outcome.Response
StatusTick = Some storyWorld.Tick }
| DialogueRejected(failure, next) ->
next,
@@ -585,6 +594,7 @@ module M5Interaction =
| value when value.StartsWith("chronicle") -> "年鉴已打开"
| "task panel open" -> "任务面板已打开"
| value when value.StartsWith("task|") -> value.Substring("task|".Length)
+ | value when value.StartsWith("inquiry|") -> value.Substring("inquiry|".Length)
| value when value.StartsWith("trade ok|") -> value.Substring("trade ok|".Length)
| value when value.StartsWith("trade fail|") -> "交易失败:" + value.Substring("trade fail|".Length)
| value when value.StartsWith("no dialogue") -> "当前没有可用对话"