Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
205 lines
8.4 KiB
Python
205 lines
8.4 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
生成/校验「骑士与无赖」(Knights and Knaves)谜题,并导出 puzzles.json。
|
|
|
|
每道谜题的每句话都用 csp_solver.py 里的结构化 DSL 表示(见该文件顶部说明),
|
|
既能渲染成中文题面(给 LLM 看),也能直接翻译成 python-constraint 约束来求解。
|
|
本脚本用 python-constraint 校验每题「解唯一」后才写出——这确保真值解无歧义,
|
|
同时演示了实验 5-2 的核心:把谜题形式化为 CSP 并用求解器离线求解。
|
|
|
|
约定:骑士(knight)永远说真话,无赖(knave)永远说假话。t[name]=True 表示骑士。
|
|
|
|
用法:
|
|
python build_puzzles.py # 导出内置的 12 道精选谜题(默认)
|
|
python build_puzzles.py --generate 20 # 随机生成 20 道解唯一的谜题
|
|
python build_puzzles.py --generate 20 --min-people 3 --max-people 5 --seed 7
|
|
python build_puzzles.py --output my.json # 指定输出文件
|
|
"""
|
|
import argparse
|
|
import json
|
|
import random
|
|
|
|
from csp_solver import render_nl, solve, solve_labeled
|
|
|
|
# 每题:id, 名字列表, dict{name: 结构化陈述}。中文题面由结构化陈述自动渲染,
|
|
# 但精选题保留手写的更自然的中文(见 STATEMENTS_NL 覆盖)。
|
|
CURATED = [
|
|
("kk01", ["A", "B"], {
|
|
"A": ["is", "B", "knave"],
|
|
"B": ["and", ["is", "A", "knave"], ["is", "B", "knave"]]}),
|
|
("kk02", ["A", "B"], {
|
|
"A": ["same", "A", "B"],
|
|
"B": ["diff", "A", "B"]}),
|
|
("kk03", ["A", "B"], {
|
|
"A": ["count", "knight", ">=", 1],
|
|
"B": ["is", "A", "knave"]}),
|
|
("kk04", ["A", "B", "C"], {
|
|
"A": ["is", "B", "knave"],
|
|
"B": ["is", "C", "knave"],
|
|
"C": ["and", ["is", "A", "knave"], ["is", "B", "knave"]]}),
|
|
("kk05", ["A", "B", "C"], {
|
|
"A": ["is", "B", "knight"],
|
|
"B": ["is", "C", "knave"],
|
|
"C": ["same", "A", "B"]}),
|
|
("kk06", ["A", "B", "C"], {
|
|
"A": ["same", "B", "C"],
|
|
"B": ["is", "A", "knave"],
|
|
"C": ["same", "C", "A"]}),
|
|
("kk07", ["A", "B", "C"], {
|
|
"A": ["or", ["is", "A", "knave"], ["is", "B", "knight"]],
|
|
"B": ["is", "A", "knight"],
|
|
"C": ["is", "B", "knave"]}),
|
|
("kk08", ["A", "B", "C", "D"], {
|
|
"A": ["same", "B", "D"],
|
|
"B": ["is", "C", "knave"],
|
|
"C": ["is", "D", "knight"],
|
|
"D": ["diff", "B", "C"]}),
|
|
("kk09", ["A", "B", "C", "D"], {
|
|
"A": ["is", "B", "knight"],
|
|
"B": ["is", "C", "knave"],
|
|
"C": ["is", "D", "knight"],
|
|
"D": ["diff", "A", "B"]}),
|
|
("kk10", ["A", "B", "C", "D"], {
|
|
"A": ["count", "knave", ">=", 3],
|
|
"B": ["is", "A", "knave"],
|
|
"C": ["is", "B", "knight"],
|
|
"D": ["is", "C", "knave"]}),
|
|
("kk11", ["A", "B", "C", "D", "E"], {
|
|
"A": ["is", "B", "knight"],
|
|
"B": ["is", "C", "knave"],
|
|
"C": ["is", "D", "knight"],
|
|
"D": ["is", "E", "knave"],
|
|
"E": ["count", "knight", ">=", 2]}),
|
|
("kk12", ["A", "B", "C", "D", "E"], {
|
|
"A": ["is", "B", "knight"],
|
|
"B": ["is", "C", "knave"],
|
|
"C": ["is", "D", "knave"],
|
|
"D": ["is", "E", "knight"],
|
|
"E": ["same", "A", "C"]}),
|
|
]
|
|
|
|
# 精选题的手写中文题面(比自动渲染更自然)。未覆盖的句子回退到 render_nl。
|
|
STATEMENTS_NL = {
|
|
("kk01", "B"): "我们两人都不是骑士。",
|
|
("kk02", "A"): "我和 B 是同一类人(要么都是骑士,要么都是无赖)。",
|
|
("kk02", "B"): "我和 A 是不同类人。",
|
|
("kk03", "A"): "我们当中至少有一个骑士。",
|
|
("kk04", "C"): "A 和 B 都是无赖。",
|
|
("kk06", "C"): "我和 A 是同一类人。",
|
|
("kk07", "A"): "我是无赖,或者 B 是骑士。",
|
|
("kk09", "D"): "A 和 B 不是同一类人。",
|
|
("kk10", "A"): "我们四人当中至少有三个无赖。",
|
|
("kk11", "E"): "我们五人当中至少有两个骑士。",
|
|
("kk12", "E"): "A 和 C 是同一类人。",
|
|
}
|
|
|
|
|
|
def build_puzzle(pid, names, structs, nl_overrides=None):
|
|
"""求解校验(要求解唯一)并组装成写入 puzzles.json 的一条记录。"""
|
|
sols = solve_labeled(names, structs)
|
|
if len(sols) != 1:
|
|
raise ValueError(f"{pid} 解不唯一: {len(sols)} 个解 -> {sols}")
|
|
solution = sols[0]
|
|
|
|
nl_overrides = nl_overrides or {}
|
|
statements = {n: nl_overrides.get(n, render_nl(structs[n])) for n in names}
|
|
lines = [f"{n}: 「{statements[n]}」" for n in names]
|
|
desc = (
|
|
f"这座岛上有 {len(names)} 位居民:{', '.join(names)}。"
|
|
"每位居民要么是永远说真话的骑士(knight),要么是永远说假话的无赖(knave)。"
|
|
"他们各自说了如下的话:\n" + "\n".join(lines)
|
|
)
|
|
return dict(id=pid, num_people=len(names), names=names,
|
|
statements=statements, statements_struct=structs,
|
|
description=desc, solution=solution)
|
|
|
|
|
|
# ---------------- 随机生成器 ----------------
|
|
def _random_stmt(speaker, names, rng):
|
|
"""为 speaker 随机生成一句合法的结构化陈述。"""
|
|
others = [n for n in names if n != speaker]
|
|
# Solo resident: only count statements are valid (no "others" to name).
|
|
if not others:
|
|
role = rng.choice(["knight", "knave"])
|
|
op = rng.choice([">=", "<=", "=="])
|
|
return ["count", role, op, rng.randint(1, len(names))]
|
|
kind = rng.choice(["is", "is", "same", "diff", "count"])
|
|
if kind == "is":
|
|
return ["is", rng.choice(others), rng.choice(["knight", "knave"])]
|
|
if kind == "same":
|
|
return ["same", speaker, rng.choice(others)]
|
|
if kind == "diff":
|
|
return ["diff", speaker, rng.choice(others)]
|
|
# count:全体中某角色的人数满足某比较
|
|
role = rng.choice(["knight", "knave"])
|
|
op = rng.choice([">=", "<=", "=="])
|
|
k = rng.randint(1, len(names))
|
|
return ["count", role, op, k]
|
|
|
|
|
|
def generate(count, min_people, max_people, seed):
|
|
"""随机生成 count 道「解唯一」的谜题(用 python-constraint 过滤)。"""
|
|
rng = random.Random(seed)
|
|
names_pool = ["A", "B", "C", "D", "E", "F", "G"]
|
|
puzzles = []
|
|
attempts = 0
|
|
while len(puzzles) < count and attempts < count * 2000:
|
|
attempts += 1
|
|
n = rng.randint(min_people, max_people)
|
|
names = names_pool[:n]
|
|
structs = {sp: _random_stmt(sp, names, rng) for sp in names}
|
|
if len(solve(names, structs)) != 1: # 只保留解唯一的谜题
|
|
continue
|
|
pid = f"gen{len(puzzles) + 1:03d}"
|
|
puzzles.append(build_puzzle(pid, names, structs))
|
|
if len(puzzles) < count:
|
|
print(f"警告:{attempts} 次尝试只生成了 {len(puzzles)}/{count} 道解唯一的谜题。")
|
|
return puzzles
|
|
|
|
|
|
def build_curated():
|
|
out = []
|
|
for pid, names, structs in CURATED:
|
|
nl = {n: STATEMENTS_NL[(pid, n)]
|
|
for n in names if (pid, n) in STATEMENTS_NL}
|
|
out.append(build_puzzle(pid, names, structs, nl))
|
|
return out
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(
|
|
description="生成/校验骑士与无赖谜题并导出 puzzles.json"
|
|
"(用 python-constraint 离线求解,校验每题解唯一)",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog=__doc__)
|
|
ap.add_argument("--generate", type=int, metavar="N", default=0,
|
|
help="随机生成 N 道解唯一的谜题(默认 0=导出内置 12 道精选题)")
|
|
ap.add_argument("--min-people", type=int, default=2,
|
|
help="随机生成时每题最少居民数(默认 2)")
|
|
ap.add_argument("--max-people", type=int, default=5,
|
|
help="随机生成时每题最多居民数(难度上限,默认 5)")
|
|
ap.add_argument("--seed", type=int, default=42,
|
|
help="随机种子,保证可复现(默认 42)")
|
|
ap.add_argument("--output", default="puzzles.json",
|
|
help="输出文件路径(默认 puzzles.json)")
|
|
args = ap.parse_args()
|
|
|
|
if args.generate > 0:
|
|
print(f"随机生成 {args.generate} 道谜题"
|
|
f"({args.min_people}~{args.max_people} 人,seed={args.seed})...")
|
|
out = generate(args.generate, args.min_people, args.max_people, args.seed)
|
|
else:
|
|
out = build_curated()
|
|
|
|
for p in out:
|
|
print(f"{p['id']}: OK 唯一解 = {p['solution']}")
|
|
|
|
with open(args.output, "w", encoding="utf-8") as f:
|
|
json.dump(out, f, ensure_ascii=False, indent=2)
|
|
print(f"\n已写出 {len(out)} 题到 {args.output}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|