293 lines
19 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://wanxiang.ai/contracts/play-loop/play-spec.schema.json",
"title": "PlaySpec",
"description": "PlaySpec 考卷契约(生成引擎 agentic 架构设计代号 C52026-07-03 一次性裁决 Δ1。一局游戏声明「该怎么玩我、以便据此判我」的那份考卷判分门play.cdp.cjs 九门)不认游戏本身,只认这份 spec——spec 说清用哪种驱动器去玩、玩前要不要先起局、玩后看哪些可观测量算过。它落盘为每局工程目录下的 play-spec.jsongame-runtime/games/_wg1-gen/<id>/play-spec.json或 tier2 工程的 play-spec.json生产侧现由 cheap_run.ensure_play_spec 据 forensics state 自动产、由 tier2 run_gates 落盘。本契约把这些至今散在代码里的隐形约定钉成显式字段,让两档(便宜档 / tier2 富游戏档)产出的 spec 同形同义。为什么现在钉:现行 cheap_run.py:241-244 用「state 里有没有 targets 键」这一隐形约定选驱动器、现行 play-spec.json 与源工程之间没有任何版本绑定cheap_run.py:278『已存在不覆盖』是缺绑定时的粗糙兜底源工程改了、旧 spec 还在,就永远吃陈考卷),这些都需要一份契约来固定语义并让陈旧可判定。消费方=W-S1 修复三单(把 startRitual / derivedFrom 接进 harness 与 ensure_play_spec与 tier2 F-1 对齐。红线:字段不编码 tier 枚举Δ9③档位差异一律用能力字段的『在不在』economy / controlCheck / firstPlay 出现 = 该局具备该能力)和 ext 扩展段表达,绝不出现 tier:cheap|tier2。",
"type": "object",
"required": ["derivedFrom"],
"anyOf": [
{ "required": ["driver"] },
{ "required": ["inputs"] }
],
"properties": {
"schemaVersion": {
"description": "契约版本标签(可选,建议填 'play-spec/1')。新增字段不升版本;删改字段或改语义才升版本,便于 harness 与 ensure_play_spec 判定自己在读哪一代考卷。",
"type": "string",
"minLength": 1
},
"derivedFrom": {
"description": "派生绑定这份考卷是从哪份源工程派生出来的。spec 由源工程派生源工程一改sourceHash 变),旧 spec 即算陈旧、必须重新生成——否则会拿旧考卷判新工程(假绿或错判)。这正是现行 cheap_run.py:278『play-spec.json 已存在就不覆盖』留下的坑:它没有任何绑定,无从知道源工程是否已经变过,于是永远沿用第一次产出的 spec。本段令陈旧可判定harness / ensure_play_spec 只要比对当前源工程 hash 与此处 sourceHash 不一致,就该重生 spec。现网 play-spec.json 尚无此段,接线在 W-S1 三单在途。",
"type": "object",
"required": ["sourceHash"],
"properties": {
"sourceHash": {
"description": "源工程内容哈希(如 sha256 十六进制串,或工程 store 的 versionId 派生 hash。它是 spec↔源工程的绑定锚值变=源工程变=spec 须重生。非空。",
"type": "string",
"minLength": 1
},
"versionId": {
"description": "源工程版本 ID可选。tier2 长生命周期项目同一 gameId 改源重建会产多个版本,落库寻址用(对接 tier2-source-project 版本寻址);传输形态兼容数字与字符串。",
"type": ["string", "integer"]
},
"generatedAt": {
"description": "本 spec 生成时刻可选ISO-8601 或毫秒时间戳字符串),供排障与陈旧度观测。",
"type": ["string", "integer"]
},
"generator": {
"description": "产出方标识(可选),如 'cheap_run.ensure_play_spec' / 'tier2.run_gates',供追溯是哪条线、哪个版本的产出逻辑写的这份考卷。",
"type": "string"
},
"regenerateOnSourceChange": {
"description": "语义声明(可选,默认真):源工程 hash 变化时是否必须重生 spec。留字段是为了把『源变即重生』这条规则写进考卷本身而不是只活在某段代码注释里。",
"type": "boolean"
}
},
"additionalProperties": false
},
"startRitual": {
"description": "起局仪式(可选):从『装载完成』到『可被驱动器真玩』之间,需要先做的一次性动作。多数游戏装载即进游玩态,省略本段即可;但有的游戏停在菜单/开始页驱动器直接开跑会一直点在死菜单上played=false、G_input/H_progress 必挂)。这时要声明先点开始按钮或先走一小段固定输入。现网 play-spec.json 目前把这类前置动作混在 inputs 数组开头(如先 wait 再 tap本段把它显式拆出来让 harness 先完成起局、再交给 driver 判分。接线在 W-S1 三单在途。",
"type": "object",
"required": ["kind"],
"properties": {
"kind": {
"description": "起局方式auto=装载即可玩无需仪式等价省略本段tap-selector=点一个具名元素如开始按钮进入游玩input-sequence=走一小段固定输入进入游玩。",
"type": "string",
"enum": ["auto", "tap-selector", "input-sequence"]
},
"selector": {
"description": "kind=tap-selector 时必填:要点击进入游玩的元素标识(如 'menuStartBtn' 这类游戏侧约定的可点起局元素)。",
"type": "string",
"minLength": 1
},
"inputs": {
"description": "kind=input-sequence 时必填:进入游玩前要走的固定输入序列,元素同顶层 inputs 的输入指令。",
"type": "array",
"items": { "$ref": "#/$defs/inputEvent" }
},
"why": {
"description": "为什么需要这道起局仪式(人读一句,供排障与续修 agent 理解)。",
"type": "string"
}
},
"additionalProperties": false,
"allOf": [
{
"if": { "properties": { "kind": { "const": "tap-selector" } }, "required": ["kind"] },
"then": { "required": ["selector"] }
},
{
"if": { "properties": { "kind": { "const": "input-sequence" } }, "required": ["kind"] },
"then": { "required": ["inputs"] }
}
]
},
"driver": {
"description": "驱动器driver 与 inputs 至少择一适配性真玩器读游戏可观测状态自动接招如接球、点亮的目标、循环按键比固定坐标序列更能真玩到技巧类游戏、少假阴。type 是驱动器族,不是 tier族的选择依据是游戏暴露了什么可观测状态——现行 cheap_run.py:241-242 的隐形约定是『state 里有 targets/target 键 → tap-targets occupied 族,否则 → key-cycle 族』。本契约把驱动器族显式化,并允许在 selectionBasis 里记下这条选择依据,让考卷自解释、不必回头读 _build_play_spec 的代码。",
"$ref": "#/$defs/driver"
},
"inputs": {
"description": "固定输入序列driver 与 inputs 至少择一):没有 driver 时按此序列逐步派发play.cdp.cjs:986-993。适合动作固定、无需读态接招的游戏或作为起局/兜底。元素见 $defs/inputEvent。",
"type": "array",
"items": { "$ref": "#/$defs/inputEvent" }
},
"expectedEngineCallPrefixes": {
"description": "F_wiring 门的语义期望:真玩期间应发生的引擎能力调用前缀集(如 'particles.' / 'audio.synth' / 'sessionScore.'。这是外生的预置期望held 期望),绝不能从本局实际 __engineCalls 反推——一旦反推期望恒等于实际、F_wiring 退化成自证必过的假绿cheap_run.py:230-231 的防自证铁律)。空数组=只要有任意引擎调用即算接线。",
"type": "array",
"items": { "type": "string" }
},
"assertAfterPlay": {
"description": "玩后进展断言(赢/输可观测量的『进展』一半真玩前后各读一次可观测状态逐条判是否按预期变化证明这局不是七门全过的空心壳play.cdp.cjs:1072 checkAssertion。每条见 $defs/assertion。空/省略=不判进展(老游戏向后兼容走 H 门 SKIP。",
"type": "array",
"items": { "$ref": "#/$defs/assertion" }
},
"expectLatch": {
"description": "是否要求真玩到终局态并驻留(赢/输可观测量的『终局』一半true=须玩到 phase 落在终局态gameover/win/lose/over 等)且不回弹;未驱动到终态但有真进展时降 advisory 不致命(限时/无快速失败态类无进展才致命play.cdp.cjs:1077-1094。省略=不判终局H 门 SKIP。",
"type": "boolean"
},
"latchTerminals": {
"description": "终局态同义词集(可选):判『玩到终局』时认哪些 phase 值为终态(默认 gameover/over/win/lose。让终态叫 win/lose 的游戏不被只认 gameover 的判据误伤。",
"type": "array",
"items": { "type": "string" }
},
"exportState": {
"description": "本局承诺经 _forensicsView().state() 暴露的可观测键(可选,如 phase/score/targets/remaining。它是 assertAfterPlay 的 path 与终局判定的取数来源;声明了机制门却读不到 state=可测性红线违约、判否play.cdp.cjs:1070。",
"type": "array",
"items": { "type": "string" }
},
"controlCheck": {
"description": "控制跟手门配置(能力字段,非 tier声明它就启用 I_control 门,验玩家控制体(如挡板 paddle.x平滑逼近触点、逮『一格一跳/卡死/不跟手』play.cdp.cjs:972、595 runControlCheck。省略=I_control SKIP。它出现与否表达的是该局有没有可跟手的控制体这一能力不是档位。",
"type": "object",
"required": ["paddlePath"],
"properties": {
"paddlePath": {
"description": "控制体位置在可观测状态里的路径(如 'paddle.x')。",
"type": "string",
"minLength": 1
},
"paddleY": { "description": "控制体所在的 y点击目标行可选。", "type": "number" },
"tapXs": { "description": "验跟手时连点的一组目标 x可选。", "type": "array", "items": { "type": "number" } },
"tapsPerTarget": { "description": "每个目标连点几拍,可选。", "type": "integer" },
"moveMin": { "description": "判『真移动了』的最小位移阈值,可选。", "type": "number" },
"tolerance": { "description": "判『逼近到位』的容差,可选。", "type": "number" },
"why": { "description": "这道控制门在验什么(人读一句)。", "type": "string" }
},
"additionalProperties": true
},
"firstPlay": {
"description": "首玩核心闭环门配置(能力字段,非 tier声明它就启用『首次真玩能否在时限内达成品类核心反馈闭环』的观测play.cdp.cjs:1100coreLoopReached 从 H 门派生 + 时限维度)。省略=不做首玩时限判定。",
"type": "object",
"properties": {
"coreLoopThresholdMs": {
"description": "核心闭环达成的墙钟时限(毫秒),如 60000。",
"type": "integer",
"minimum": 1
}
},
"additionalProperties": true
},
"economy": {
"description": "经济能力段(能力字段,非 tier声明它=该局有经营/经济胜负语义,经济门读它的验收外生赢线 winThreshold 判『盈利路能否在有界真玩内到赢态』tier2 run.py:1000 _economy_evidence 读 spec.economy.winThreshold。便宜档轻游戏不含此段。它的『在不在』表达能力差异故无需 tier 枚举。",
"type": "object",
"properties": {
"winThreshold": {
"description": "验收外生赢线品类金标锚定、agent 不可调):真玩要在此金额/指标线之上才算盈利路通过。",
"type": "number"
}
},
"additionalProperties": true
},
"ext": {
"description": "扩展段Δ9③某一档独有、不进对称核心的考卷字段塞这里各写各的、没有的字段绝不编造。便宜档一般不写tier2 富游戏档在此放它独有的判分旋钮。留此段是为了在不加 tier 枚举的前提下容纳档位差异——接口对称上面的核心字段两档同名同义内容不对称ext 各写各的)。",
"type": "object",
"additionalProperties": true
}
},
"patternProperties": {
"^_": {
"description": "下划线开头的作者元数据(如 _genre / _baseSample / _note供人读与金标标注harness 不消费。放行任意值。"
}
},
"additionalProperties": false,
"$defs": {
"driver": {
"type": "object",
"required": ["type"],
"properties": {
"type": {
"description": "驱动器族(非 tier现有族=key-cycle循环按方向键如 2048/俄罗斯方块、tap-targets点可点目标配 targetMode/targetsPath如打地鼠/点击得分/经营点客、paddle-intercept读球位自动移挡板接球如打砖块/弹球。新增族可扩harness 的 runDriver 须认得该 type。",
"type": "string",
"minLength": 1
},
"selectionBasis": {
"description": "驱动器族的选择依据(可选,人读一句):为什么这局选这个族。把 cheap_run.py:241-242『state 有 targets/target 键 → tap-targets否则 → key-cycle』这条隐形约定写进考卷本身让 spec 自解释。",
"type": "string"
},
"keys": {
"description": "key-cycle 族必填:循环派发的按键序列(如 ['ArrowLeft','ArrowRight','ArrowUp','ArrowDown'])。",
"type": "array",
"items": { "type": "string" }
},
"targetMode": {
"description": "tap-targets 族的目标选取模式(如 'occupied'=点被占用/亮起的格位)。",
"type": "string"
},
"targetsPath": {
"description": "tap-targets 族必填:可点目标数组在可观测状态里的路径(如 'targets')。",
"type": "string"
},
"ballPath": {
"description": "paddle-intercept 族必填:被接物(球)位置在可观测状态里的路径(如 'ball.x')。",
"type": "string"
},
"paddleY": { "description": "paddle-intercept 族:挡板所在 y。", "type": "number" },
"steps": { "description": "驱动步数(真玩多少拍)。", "type": "integer", "minimum": 1 },
"stepMs": { "description": "每拍间隔毫秒。", "type": "integer", "minimum": 1 },
"downMs": { "description": "key 类按下时长毫秒(可选)。", "type": "integer", "minimum": 0 },
"winThreshold": { "description": "驱动器内联的验收赢线(可选,与顶层 economy.winThreshold 同义、tier2 早期形态)。", "type": "number" }
},
"additionalProperties": true,
"allOf": [
{
"if": { "properties": { "type": { "const": "key-cycle" } }, "required": ["type"] },
"then": { "required": ["keys"] }
},
{
"if": { "properties": { "type": { "const": "tap-targets" } }, "required": ["type"] },
"then": { "required": ["targetsPath"] }
},
{
"if": { "properties": { "type": { "const": "paddle-intercept" } }, "required": ["type"] },
"then": { "required": ["ballPath"] }
}
]
},
"inputEvent": {
"type": "object",
"required": ["t"],
"properties": {
"t": {
"description": "输入指令类型tap=点击(配 x,y、key=按键(配 code可选 downMs、drag=拖拽(配 from,to可选 ms、wait=等待(配 ms。",
"type": "string",
"enum": ["tap", "key", "drag", "wait"]
},
"x": { "description": "tap 的 x 坐标。", "type": "number" },
"y": { "description": "tap 的 y 坐标。", "type": "number" },
"code": { "description": "key 的键码(如 'ArrowUp' / 'Space')。", "type": "string" },
"downMs": { "description": "key 按下时长毫秒(可选)。", "type": "integer", "minimum": 0 },
"from": { "description": "drag 起点 [x,y]。", "type": "array", "items": { "type": "number" } },
"to": { "description": "drag 终点 [x,y]。", "type": "array", "items": { "type": "number" } },
"ms": { "description": "wait/drag 的毫秒时长。", "type": "integer", "minimum": 0 }
},
"additionalProperties": true,
"allOf": [
{
"if": { "properties": { "t": { "const": "tap" } }, "required": ["t"] },
"then": { "required": ["x", "y"] }
},
{
"if": { "properties": { "t": { "const": "key" } }, "required": ["t"] },
"then": { "required": ["code"] }
},
{
"if": { "properties": { "t": { "const": "wait" } }, "required": ["t"] },
"then": { "required": ["ms"] }
},
{
"if": { "properties": { "t": { "const": "drag" } }, "required": ["t"] },
"then": { "required": ["from", "to"] }
}
]
},
"assertion": {
"type": "object",
"required": ["path", "op"],
"properties": {
"path": {
"description": "被断言的可观测状态路径(如 'score' / 'remaining')。非空。",
"type": "string",
"minLength": 1
},
"op": {
"description": "比较算子play.cdp.cjs:188 checkAssertion 支持集increased/decreased/changed 不带 value>、>=、<、<=、== 带 value 阈值in、reached 带 value 允许集数组。用集合外的算子 harness 会静默判否pass 恒 false故锁为闭集。",
"type": "string",
"enum": ["increased", "decreased", "changed", ">", ">=", "<", "<=", "==", "in", "reached"]
},
"value": {
"description": "阈值或允许集op 属于 {>,>=,<,<=,==} 时为阈值标量、op 属于 {in,reached} 时为允许集数组increased/decreased/changed 不需要本字段。"
},
"why": {
"description": "这条断言在证明什么(人读一句,未过时回喂续修 agent。",
"type": "string"
}
},
"additionalProperties": false,
"allOf": [
{
"if": { "properties": { "op": { "enum": [">", ">=", "<", "<=", "==", "in", "reached"] } }, "required": ["op"] },
"then": { "required": ["value"] }
}
]
}
}
}