15.3 指标不能声称什么

命令行量不出玩家情绪,本卷七条指标全是 proxy。本节把「不能声称什么」写成可执行的 lint:八类禁声称(更感人、更好玩、玩家满意度、更优秀、玩家实际、读起来、绝对达标、提升 N 倍)、五条正面清单,以及 12 条候选结论的逐条判定与改写。指标能声称的只有相对变化与结构量。

《游戏设计》卷一《玩法机制与可执行规格》 那一套给的是玩法规格的度量口径;本章给的是叙事规格的度量口径——指标集不同(那边是一局的动力学,这边是整张故事图的路径形状),但「整数化 + 确定性 + 只做相对比较」的纪律同源。15.1 收表、15.2 划界,15.3 是全卷的落点:把这些数不能声称什么,写成一份可执行的清单。

15.3 指标不能声称什么

本卷通篇在度量叙事体验,但有一条不可回避的事实:命令行量不出玩家情绪。真人测试需要真实业务环境,命令行不可复现,本套不做。所以七条指标无一例外是 proxy(代理指标),只能做相对比较。这一节把这条纪律从一句口号,变成三件可跑的事:全表 proxy 声明完整性、候选结论语料 lint、正面清单自查。

禁声称清单

本卷点名禁的表述有八类:

禁声称为什么禁
更感人 / 更好玩主观体验的绝对断言
玩家满意度 X%把 proxy 当成满意度调查
更优秀跨作品 / 跨规格的绝对比较
玩家实际把结构量当成玩家真实体验
读起来主观可读性判断
绝对达标绝对化断言
提升 N 倍倍数化的效果声称

这八类有个共同点:都把相对量当成了绝对量。覆盖率、熵、MAE 都是规格里的结构读数,一旦被说成「更感人」「满意度 80%」,proxy 就冒充了它根本度量不到的东西。

正面清单:能声称的只有两类

禁令的另一面是许可。指标能声称的收敛成两类——相对变化与结构量:

  • 相对变化:同一规格内,采样 200 条的覆盖率 1000 千分高于单条确定性路径的 446 千分;
  • 结构量:确定性路径覆盖 25 / 56 条去重对话边,到达 1 / 5 个结局;
  • 回归对照:把 b10 的张力从 5 降到 4 后,全曲线 MAE 从 0 毫抬到 62 毫;
  • 恒等判据:整数不变量残差为 0,好感度全程落在 [0, 100];
  • 口径声明:张力曲线 MAE 是 proxy,度量的是标注与目标的偏离,不构成情感强度的证明。

注意这五条里没有一条在说「好」或「坏」——它们说的全是变化和结构。这就是本卷允许的结论形态。

S 层:把禁令写成 lint

脚本 sim/nm_15_3_claim_lint.py 做三件事:检查 15.1 的 7 条指标是否都声明了 kind=proxy、does_not_measure、allowed_use;对 12 条候选结论逐条 lint,命中禁声称即 FAIL;再验证每条 FAIL 的改写与正面清单示例都必须 PASS:

#!/usr/bin/env python3
# file: sim/nm_15_3_claim_lint.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/nm_15_3_claim_lint.py
"""卷三 15.3 指标不能声称什么(S 层):把禁令写成一份可执行的 lint。

本卷通篇在度量叙事体验,每一个指标都是 proxy。这一节把「不能声称什么」从
一句话变成三件可跑的事:

  一 全表 proxy 声明完整性:15.1 的 7 条指标逐条检查 kind=proxy、
     does_not_measure 非空、allowed_use 非空——缺一项即 FAIL。
  二 候选结论语料 lint:读 config/nm_15_3_claim_policy.json 的 12 条候选结论,
     命中禁声称模式(更感人 / 更好玩 / 玩家满意度 / 更优秀 / 玩家实际 / 读起来 /
     绝对达标 / 提升 N 倍)即 FAIL,判定必须与语料标注的 expect 一致。
  三 正面清单:每条 FAIL 的改写必须 PASS,正面清单里的每个示例也必须 PASS。

禁令清单与 15.1 定义表的 does_not_measure 是同一套边界的两种写法。只做相对
比较,不做绝对判断。产出 out/nm_15_3_claim_lint.json。
"""
from __future__ import annotations

import json
import os
import re
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
OUT = os.path.join(ROOT, "out")


def load(name: str) -> dict:
    with open(os.path.join(ROOT, name), encoding="utf-8") as fh:
        return json.load(fh)


def hits(text: str, rules: list) -> list:
    """返回命中的禁声称规则列表。substring 精确包含,regex 用 re.search。"""
    out = []
    for r in rules:
        if r["kind"] == "substring":
            if r["pattern"] in text:
                out.append(r)
        elif re.search(r["pattern"], text):
            out.append(r)
    return out


def main() -> int:
    table = load("config/nm_15_1_metric_definition_table.json")["metrics"]
    policy = load("config/nm_15_3_claim_policy.json")
    rules = policy["forbidden"]

    # ---- 一 全表 proxy 声明完整性 ----
    kinds = sum(1 for e in table if e.get("kind") == "proxy")
    dnm = sum(1 for e in table if e.get("does_not_measure"))
    use = sum(1 for e in table if e.get("allowed_use"))
    dnm_items = sum(len(e.get("does_not_measure") or []) for e in table)
    print("== 一 全表 proxy 声明完整性 ==")
    print(f"   指标 {len(table)} 条:kind=proxy {kinds} / {len(table)};"
          f"does_not_measure 非空 {dnm} / {len(table)};allowed_use 非空 {use} / {len(table)}")
    print(f"   「不度量」项合计 {dnm_items} 条;禁声称模式 {len(rules)} 条")
    assert kinds == len(table) == dnm == use, "有指标缺 proxy 声明"

    # ---- 二 候选结论语料 lint ----
    print("== 二 候选结论语料 lint ==")
    rows, agree = [], 0
    for c in policy["claims"]:
        h = hits(c["text"], rules)
        verdict = "FAIL" if h else "PASS"
        agree += int(verdict == c["expect"])
        why = ";".join(f"{x['pattern']}({x['reason']})" for x in h) if h else "未命中禁声称"
        rows.append({"id": c["id"], "verdict": verdict, "expect": c["expect"],
                     "hits": [x["pattern"] for x in h], "why": why, "text": c["text"]})
        print(f"   {c['id']:<4} {verdict:<5} {why}")
    n_pass = sum(1 for r in rows if r["verdict"] == "PASS")
    n_fail = sum(1 for r in rows if r["verdict"] == "FAIL")
    print(f"   语料 {len(rows)} 条:PASS {n_pass} / FAIL {n_fail},判定与预期一致 {agree} / {len(rows)}")
    assert agree == len(rows), "lint 判定与语料标注不一致"

    # ---- 三 正面清单:改写与示例都必须 PASS ----
    print("== 三 正面清单(改写与示例必须 PASS) ==")
    rewrites = 0
    for c in policy["claims"]:
        if c["expect"] != "FAIL":
            continue
        h = hits(c["rewrite"], rules)
        rewrites += 1
        print(f"   {c['id']:<4} 改写 {'PASS' if not h else 'FAIL'}  {c['rewrite']}")
        assert not h, f"{c['id']} 改写仍命中禁声称:{h}"
    bad_ex = [f["form"] for f in policy["allowed_forms"] if hits(f["example"], rules)]
    print(f"   改写 {rewrites} / {n_fail} 条 PASS;正面清单示例 "
          f"{len(policy['allowed_forms'])} 条,命中禁声称 {len(bad_ex)} 条")
    assert not bad_ex, f"正面清单示例命中禁声称:{bad_ex}"
    print("   结论:指标能声称的只有「相对变化」与「结构量」;倍数、满意度与主观体验一律禁")

    os.makedirs(OUT, exist_ok=True)
    with open(os.path.join(OUT, "nm_15_3_claim_lint.json"), "w", encoding="utf-8") as fh:
        json.dump({"metrics_checked": len(table), "proxy_ok": kinds,
                   "does_not_measure_items": dnm_items, "forbidden_patterns": len(rules),
                   "claims": rows, "pass": n_pass, "fail": n_fail,
                   "rewrites_ok": rewrites,
                   "allowed_forms": policy["allowed_forms"], "proxy": True},
                  fh, ensure_ascii=False, indent=1)
        fh.write("\n")
    print("wrote out/nm_15_3_claim_lint.json")
    print("OK nm_15_3_claim_lint:禁声称清单可执行,12 条语料判定一致,7 条改写全 PASS")
    return 0


if __name__ == "__main__":
    sys.exit(main())

本机实跑 /opt/homebrew/bin/python3 sim/nm_15_3_claim_lint.py:

== 一 全表 proxy 声明完整性 ==
   指标 7 条:kind=proxy 7 / 7;does_not_measure 非空 7 / 7;allowed_use 非空 7 / 7
   「不度量」项合计 20 条;禁声称模式 8 条
== 二 候选结论语料 lint ==
   C1   PASS  未命中禁声称
   C2   FAIL  更好玩(主观体验的绝对断言)
   C3   FAIL  玩家满意度(把 proxy 当成满意度调查)
   C4   PASS  未命中禁声称
   C5   FAIL  提升[^,。;]{0,10}倍(倍数化的效果声称)
   C6   PASS  未命中禁声称
   C7   FAIL  更优秀(跨作品 / 跨规格的绝对比较)
   C8   FAIL  玩家实际(把结构量当成玩家真实体验)
   C9   FAIL  读起来(主观可读性判断)
   C10  PASS  未命中禁声称
   C11  PASS  未命中禁声称
   C12  FAIL  绝对达标(绝对化断言)
   语料 12 条:PASS 5 / FAIL 7,判定与预期一致 12 / 12
== 三 正面清单(改写与示例必须 PASS) ==
   C2   改写 PASS  覆盖率从 446 千分提到 1000 千分,说明采样策略覆盖了更多对话边。
   C3   改写 PASS  熵从 1714 毫比特提到 1730 毫比特,说明采样分布的均匀度略微提高。
   C5   改写 PASS  改完之后张力 MAE 从 62 毫降到 0 毫,说明节拍标注与目标曲线重新对齐。
   C7   改写 PASS  覆盖率 1000 千分是这份规格在去重边口径下的定义上界。
   C8   改写 PASS  确定性路径覆盖 25 / 56 条去重对话边,占全部边的 446 千分。
   C9   改写 PASS  节奏密度偏差 5 千分,说明只有 5 千分的滑窗越出可读带 [250, 750]。
   C12  改写 PASS  这一节给出的是节拍标注与目标曲线的偏离,不构成情感曲线达标的证明。
   改写 7 / 7 条 PASS;正面清单示例 5 条,命中禁声称 0 条
   结论:指标能声称的只有「相对变化」与「结构量」;倍数、满意度与主观体验一律禁
wrote out/nm_15_3_claim_lint.json
OK nm_15_3_claim_lint:禁声称清单可执行,12 条语料判定一致,7 条改写全 PASS

判定与语料标注必须一致(12 / 12),这是 assert:如果一条「更感人」被判成 PASS,lint 就失效了。7 条改写也必须全 PASS——只禁不改,等于没给出口。

D 层:禁声称清单与正面清单

清单落 config/nm_15_3_claim_policy.json:8 条禁声称模式(含一条正则)+ 5 条正面清单 + 12 条候选结论(每条 FAIL 附改写):

{
  "section": "15.3",
  "tier": "D",
  "note": "禁声称清单与正面清单。lint 由 sim/nm_15_3_claim_lint.py 执行:禁声称命中即 FAIL,每条 FAIL 的改写必须 PASS。",
  "forbidden": [
    {"pattern": "更感人", "kind": "substring", "reason": "主观体验的绝对断言"},
    {"pattern": "更好玩", "kind": "substring", "reason": "主观体验的绝对断言"},
    {"pattern": "玩家满意度", "kind": "substring", "reason": "把 proxy 当成满意度调查"},
    {"pattern": "更优秀", "kind": "substring", "reason": "跨作品 / 跨规格的绝对比较"},
    {"pattern": "玩家实际", "kind": "substring", "reason": "把结构量当成玩家真实体验"},
    {"pattern": "读起来", "kind": "substring", "reason": "主观可读性判断"},
    {"pattern": "绝对达标", "kind": "substring", "reason": "绝对化断言"},
    {"pattern": "提升[^,。;]{0,10}倍", "kind": "regex", "reason": "倍数化的效果声称"}
  ],
  "allowed_forms": [
    {"form": "相对变化", "example": "同一规格内,采样 200 条的覆盖率 1000 千分高于单条确定性路径的 446 千分"},
    {"form": "结构量", "example": "确定性路径覆盖 25 / 56 条去重对话边,到达 1 / 5 个结局"},
    {"form": "回归对照", "example": "把 b10 的张力从 5 降到 4 后,全曲线 MAE 从 0 毫抬到 62 毫"},
    {"form": "恒等判据", "example": "整数不变量残差为 0,好感度全程落在 [0, 100]"},
    {"form": "口径声明", "example": "张力曲线 MAE 是 proxy,度量的是标注与目标的偏离,不构成情感强度的证明"}
  ],
  "claims": [
    {"id": "C1", "expect": "PASS",
     "text": "同一规格内,采样 200 条的覆盖率 1000 千分高于单条确定性路径的 446 千分。"},
    {"id": "C2", "expect": "FAIL",
     "text": "本节把覆盖率从 446 千分提到 1000 千分,说明这段叙事更好玩。",
     "rewrite": "覆盖率从 446 千分提到 1000 千分,说明采样策略覆盖了更多对话边。"},
    {"id": "C3", "expect": "FAIL",
     "text": "熵从 1714 提到 1730,说明玩家满意度提升了 12%。",
     "rewrite": "熵从 1714 毫比特提到 1730 毫比特,说明采样分布的均匀度略微提高。"},
    {"id": "C4", "expect": "PASS",
     "text": "一致性残差从 8 降到 0,说明规格里的四类静态失配被逐条清零。"},
    {"id": "C5", "expect": "FAIL",
     "text": "改完之后张力 MAE 降了 62 毫,说明情感体验提升了 62 倍。",
     "rewrite": "改完之后张力 MAE 从 62 毫降到 0 毫,说明节拍标注与目标曲线重新对齐。"},
    {"id": "C6", "expect": "PASS",
     "text": "整数不变量残差为 0,好感度全程落在 [0, 100]。"},
    {"id": "C7", "expect": "FAIL",
     "text": "覆盖率 1000 千分说明这份叙事比其它作品更优秀。",
     "rewrite": "覆盖率 1000 千分是这份规格在去重边口径下的定义上界。"},
    {"id": "C8", "expect": "FAIL",
     "text": "确定性路径只覆盖 446 千分,说明玩家实际探索到的丰富度只有 44.6%。",
     "rewrite": "确定性路径覆盖 25 / 56 条去重对话边,占全部边的 446 千分。"},
    {"id": "C9", "expect": "FAIL",
     "text": "节奏密度偏差 5 千分,说明玩家读起来不累。",
     "rewrite": "节奏密度偏差 5 千分,说明只有 5 千分的滑窗越出可读带 [250, 750]。"},
    {"id": "C10", "expect": "PASS",
     "text": "n=20000 时结局分布熵 1714 毫比特,均匀分布上界是 2321 毫比特。"},
    {"id": "C11", "expect": "PASS",
     "text": "张力曲线 MAE 是 proxy,度量的是作者标注与设计目标的偏离,不是情绪强度。"},
    {"id": "C12", "expect": "FAIL",
     "text": "这一节证明了这份叙事的情感曲线绝对达标。",
     "rewrite": "这一节给出的是节拍标注与目标曲线的偏离,不构成情感曲线达标的证明。"}
  ]
}

这份清单与 15.1 定义表里的 does_not_measure 是同一套边界的两种写法:定义表从「指标不度量什么」写,清单从「结论不能说什么」写。两边对上,proxy 的口径才算钉死。

判读

第一,禁令必须是可执行的,否则它只是修辞。 一句「本卷指标只做相对比较」写在散文里,没人能验证它有没有被违反。本节把它变成 8 条模式:任何一条结论,只要命中模式就 FAIL。提升[^,。;]{0,10}倍 这条正则甚至能抓住「提升了 62 倍」这种换了说法的倍数断言——禁声称清单是数据,不是声明。

第二,禁与许是一体两面。 只说「不能声称更感人」,读者会问「那能声称什么」。正面清单回答了它:相对变化、结构量、回归对照、恒等判据、口径声明。把许可也写下来,禁令才不空洞——否则纪律会变成「什么都别写」的消极沉默。

第三,本节是全卷的落点,因为它定义了「这份工作的产出形态」。 本卷跑出 446 千分、1714 毫比特、0 毫这些数,它们能进报告的只有两类句子:「A 比 B 高 / 低多少」和「结构是 X / 56」。任何一句「这段叙事更感人」「玩家满意度提升了」都不在产出之列。这不是保守,是诚实——命令行能证的只有结构,报告就只写结构。

第四,这条边界和《游戏设计》卷二是接续的。 那一套给机制指标定容差(差多少算一致),本卷给叙事 proxy 定用途(这个数能被拿来说什么)。容差管「算得准不准」,用途管「说得对不对」。两者合起来,才是「整数化 + 确定性 + proxy 口径」这条纪律在三卷之间不落空。

自检表

检查项实测合格标准
proxy 声明完整7 / 7每条 kind=proxy 且两张清单非空
语料判定一致12 / 12判定与标注全一致
改写全 PASS7 / 7每条 FAIL 都有能过检的改写
正面清单5 条,命中 0示例不含禁声称
禁声称模式8 条覆盖点名禁的表述

阅读导航:上一节:15.2 指标的可比性与局限 · 下一节:16.1 三卷的装配检查表 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「narrative」更多文章

  1. 15.3 规模告警
  2. 15.2 求值与遍历的开销
  3. 15.1 规格规模的增长