fix(core): keep play turns action-continuous

This commit is contained in:
Ma
2026-06-09 13:18:25 +08:00
parent e911c2b0cc
commit 7499b86e77
2 changed files with 35 additions and 0 deletions
@@ -147,6 +147,26 @@ describe("play agents", () => {
expect(system).toContain("自然语言状态");
});
it("mutator prompt keeps each player action to one adjacent beat instead of jumping ahead", async () => {
const agent = new PlayWorldMutatorAgent(ctx);
const chat = vi.spyOn(agent as unknown as { chat: PlayWorldMutatorAgent["chat"] }, "chat").mockResolvedValue({
content: JSON.stringify({ eventId: "evt-1", turn: 1, actionKind: "do" }),
} as never);
await agent.proposeMutation({
turn: 1,
input: "我冲下台阶去拔那把战斧",
action: { actionKind: "do", intent: "冲下台阶并拔起战斧" },
context: "当前场景:敌人正在合围,战斧在庭院废墟中。",
language: "zh",
});
const messages = chat.mock.calls[0]?.[0] as ReadonlyArray<{ readonly role: string; readonly content: string }>;
const system = messages.find((message) => message.role === "system")?.content ?? "";
expect(system).toContain("只推进相邻一拍");
expect(system).toContain("不要替玩家越过过程");
});
it("renderer treats player negation and applied time as canonical", async () => {
const prompt = buildSceneRendererSystemPrompt("open", "zh");
expect(prompt).toContain("玩家原话里的否定动作");
@@ -155,6 +175,13 @@ describe("play agents", () => {
expect(prompt).toContain("不得另写");
});
it("renderer bridges from the player's action instead of jumping to an epilogue", () => {
const prompt = buildSceneRendererSystemPrompt("open", "zh");
expect(prompt).toContain("先承接玩家动作");
expect(prompt).toContain("不要直接跳到动作完成后");
expect(prompt).toContain("不要写总结性尾声");
});
it("renders the applied state as prose plus suggested actions", async () => {
const agent = new PlaySceneRendererAgent(ctx);
vi.spyOn(agent as unknown as { chat: PlaySceneRendererAgent["chat"] }, "chat").mockResolvedValue({
+8
View File
@@ -394,6 +394,8 @@ function buildWorldMutatorSystemPrompt(language: "zh" | "en"): string {
"You are an interactive-fiction world-state drafter.",
"Based only on the player's action and the current context, propose this turn's possible state changes as a draft.",
"Do not write final prose; do not commit to the store on the reducer's behalf; do not let key states jump to completion out of nowhere.",
"One player input advances one adjacent beat. If the player gives a chain of actions, apply only the literal chain up to the nearest new pressure point; do not skip through off-screen aftermath, rewards, or resolution beyond what the input directly attempts.",
"Do not leap over the process. If the player runs toward, reaches for, uses, opens, or confronts something, account for the movement, resistance, interruption, or immediate pressure inside this same beat instead of jumping straight to an after-the-fact state.",
"This engine is genre-neutral: romance, adventure, wuxia, mystery, slice-of-life all use the same structure. Entity types: actor/location/item/evidence/clue/claim/proof_chain/organization/rule/scene/event — use as needed.",
"Give every new or important entity a one-line summary (who/what it is and why it matters), not just a status word — the player expands this summary in the side panel.",
"Tangible things the player discovers or holds (a clue, a document, a weapon, a token, key evidence) MUST be their own entity (item/evidence/clue), never folded into a person's status — only then can they enter the player's holdings and be tracked. Observed phenomena, knowledge, impressions, or environmental signs are NOT holdings.",
@@ -418,6 +420,8 @@ function buildWorldMutatorSystemPrompt(language: "zh" | "en"): string {
"你是互动小说世界状态草案员。",
"你只根据玩家动作和当前上下文,提出本回合可能发生的状态变化草案。",
"不要写最终正文;不要越权替 reducer 落库;不要凭空让关键状态一步到位。",
"一个玩家输入只推进相邻一拍。玩家把多个动作写在一句里时,只处理原话直接包含的动作链,到最近的新压力点就停;不要跳过过程去写场外后果、完整回报或问题解决。",
"不要替玩家越过过程。玩家奔向、伸手、使用、打开或对峙某物时,必须把移动、阻力、干扰、敌人压近或即时压力算进这一拍,不能直接跳到事后状态。",
"这套引擎是品类中立的:恋情、冒险、武侠、悬疑、日常等都用同一套结构表达。实体类型用 actor/location/item/evidence/clue/claim/proof_chain/organization/rule/scene/event,按需选用。",
"给每个新出现或重要的实体写一句 summary(他是谁/这是什么、为什么重要),不要只靠 status 一句话——玩家会在侧栏里展开看这条 summary。",
"玩家发现或获得的「实物」(线索、文件、凶器、信物、关键证据等)必须建成独立实体(item/evidence/clue),不要塞进某个人物的 status——这样它们才能进入玩家的「持有物」并被追踪。观察到的现象、知识、印象、环境征兆不是持有物。",
@@ -477,6 +481,8 @@ export function buildSceneRendererSystemPrompt(mode: "open" | "guided" = "open",
"Write the response only from the already-applied state; do not overturn the reducer's results.",
"Concrete new objects, clues, evidence, locations, organizations, or named people can only appear if they are already present in Applied changes or Current state summary. If the prose needs a new concrete thing, it must have been created by the mutator first; otherwise describe mood, pressure, or an unnamed detail instead.",
"It should read like a playable novel — action, senses, pressure, breathing room — never a system log and never a menu-narration that herds the player into picking something.",
"Bridge from the player's action first. Even though the state is already applied, do not start as if everything is already over; write the follow-through, contact, resistance, interruption, and immediate consequence so the action connects to the new state.",
"Do not jump straight to the after-action result, and do not write epilogue-style summaries, morals, or closing-theme lines. End on an immediate sensory pressure, changed position, exposed detail, or nearby consequence.",
"Stay strictly inside the world the premise established — era, place, tech level, genre tone must stay consistent. Never introduce elements that don't belong: a modern-city story must not grow night-watchmen / oil lamps; a historical/wuxia story must not sprout phones / cars / computers. Every detail lands inside the given world.",
// Presence is a valid turn.
"The player is not always 'acting'. When they merely observe, linger, feel, idle-chat, or do nothing, give an immersive beat — one living detail, a smell, a bystander's small movement, a thought crossing their mind. NEVER say 'there's nothing more to see' / 'you already looked' / 'stop stalling', and never nag them to hurry up and act. Let the beat breathe.",
@@ -499,6 +505,8 @@ export function buildSceneRendererSystemPrompt(mode: "open" | "guided" = "open",
"你只能根据已经应用后的状态写回应,不要推翻 reducer 结果。",
"具体的新物件、线索、证据、地点、组织、具名人物,只能来自「已应用的本回合变化」或「当前状态摘要」。如果正文需要一个新的具体东西,它必须先由 mutator 建成实体;否则只写氛围、压力或不具名的细节。",
"回应要像可玩的小说:有动作、感官、压迫、留白;绝不是系统日志,也绝不是把玩家往'快做个选择'上赶的菜单旁白。",
"先承接玩家动作。即使状态已经应用,也不要直接跳到动作完成后;要写出动作的跟进、接触、阻力、打断和即时后果,让玩家原话自然接到新状态。",
"不要写总结性尾声、主题升华或收束感很强的结语。每回合停在眼前的感官压力、位置变化、暴露出的细节或近处后果上。",
"严格守住前提确立的那个世界——年代、地点、技术水平、题材基调都要一致。绝不要引入不属于它的元素:现代都市故事别冒出更夫/油灯/二更天,古代武侠故事别冒出手机/汽车/电脑。每一拍的细节都落在前提给定的那个世界里。",
// 在场即合法
"玩家不一定每回合都在'行动'。当他只是观察、停留、感受、闲聊、发呆,给一段有沉浸感的回应——一个活的细节、一缕气味、旁人的一个小动作、心里掠过的一个念头。绝不要说'这里没什么可看的了''你已经看过了''别磨蹭',也绝不要催他快点行动。让这一拍能呼吸。",